629 lines
22 KiB
JavaScript
629 lines
22 KiB
JavaScript
import utils from '../../utils';
|
|
import networkSvc from '../../networkSvc';
|
|
import store from '../../../store';
|
|
|
|
const clientId = GOOGLE_CLIENT_ID;
|
|
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
|
const appsDomain = null;
|
|
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h)
|
|
let gapi;
|
|
let google;
|
|
|
|
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
|
|
const getDriveScopes = token => [token.driveFullAccess
|
|
? 'https://www.googleapis.com/auth/drive'
|
|
: 'https://www.googleapis.com/auth/drive.file',
|
|
'https://www.googleapis.com/auth/drive.install'];
|
|
const bloggerScopes = ['https://www.googleapis.com/auth/blogger'];
|
|
const photosScopes = ['https://www.googleapis.com/auth/photos'];
|
|
|
|
const libraries = ['picker'];
|
|
|
|
const checkIdToken = (idToken) => {
|
|
try {
|
|
const token = idToken.split('.');
|
|
const payload = JSON.parse(utils.decodeBase64(token[1]));
|
|
return payload.aud === clientId && Date.now() + tokenExpirationMargin < payload.exp * 1000;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
let driveState;
|
|
if (utils.queryParams.providerId === 'googleDrive') {
|
|
try {
|
|
driveState = JSON.parse(utils.queryParams.state);
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
export default {
|
|
folderMimeType: 'application/vnd.google-apps.folder',
|
|
driveState,
|
|
driveActionFolder: null,
|
|
driveActionFiles: [],
|
|
request(token, options) {
|
|
return networkSvc.request({
|
|
...options,
|
|
headers: {
|
|
...options.headers || {},
|
|
Authorization: `Bearer ${token.accessToken}`,
|
|
},
|
|
}, true)
|
|
.catch((err) => {
|
|
const reason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
|
|
if (reason === 'authError') {
|
|
// Mark the token as revoked and get a new one
|
|
store.dispatch('data/setGoogleToken', {
|
|
...token,
|
|
expiresOn: 0,
|
|
});
|
|
// Refresh token and retry
|
|
return this.refreshToken(token, token.scopes)
|
|
.then(refreshedToken => this.request(refreshedToken, options));
|
|
}
|
|
throw err;
|
|
});
|
|
},
|
|
uploadFileInternal(
|
|
refreshedToken,
|
|
name,
|
|
parents,
|
|
appProperties,
|
|
media = null,
|
|
mediaType = null,
|
|
fileId = null,
|
|
oldParents = null,
|
|
ifNotTooLate = cb => res => cb(res),
|
|
) {
|
|
return Promise.resolve()
|
|
// Refreshing a token can take a while if an oauth window pops up, make sure it's not too late
|
|
.then(ifNotTooLate(() => {
|
|
const options = {
|
|
method: 'POST',
|
|
url: 'https://www.googleapis.com/drive/v3/files',
|
|
};
|
|
const params = {
|
|
supportsTeamDrives: true,
|
|
};
|
|
const metadata = { name, appProperties };
|
|
if (fileId) {
|
|
options.method = 'PATCH';
|
|
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
|
if (parents && oldParents) {
|
|
params.addParents = parents
|
|
.filter(parent => oldParents.indexOf(parent) === -1)
|
|
.join(',');
|
|
params.removeParents = oldParents
|
|
.filter(parent => parents.indexOf(parent) === -1)
|
|
.join(',');
|
|
}
|
|
} else if (parents) {
|
|
metadata.parents = parents;
|
|
}
|
|
if (media) {
|
|
const boundary = `-------${utils.uid()}`;
|
|
const delimiter = `\r\n--${boundary}\r\n`;
|
|
const closeDelimiter = `\r\n--${boundary}--`;
|
|
let multipartRequestBody = '';
|
|
multipartRequestBody += delimiter;
|
|
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
|
multipartRequestBody += JSON.stringify(metadata);
|
|
multipartRequestBody += delimiter;
|
|
multipartRequestBody += `Content-Type: ${mediaType || 'text/plain'}; charset=UTF-8\r\n\r\n`;
|
|
multipartRequestBody += media;
|
|
multipartRequestBody += closeDelimiter;
|
|
options.url = options.url.replace(
|
|
'https://www.googleapis.com/',
|
|
'https://www.googleapis.com/upload/');
|
|
return this.request(refreshedToken, {
|
|
...options,
|
|
params: {
|
|
...params,
|
|
uploadType: 'multipart',
|
|
},
|
|
headers: {
|
|
'Content-Type': `multipart/mixed; boundary="${boundary}"`,
|
|
},
|
|
body: multipartRequestBody,
|
|
}).then(res => res.body);
|
|
}
|
|
if (mediaType) {
|
|
metadata.mimeType = mediaType;
|
|
}
|
|
return this.request(refreshedToken, {
|
|
...options,
|
|
body: metadata,
|
|
params,
|
|
}).then(res => res.body);
|
|
}));
|
|
},
|
|
downloadFileInternal(refreshedToken, id) {
|
|
return this.request(refreshedToken, {
|
|
method: 'GET',
|
|
url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`,
|
|
raw: true,
|
|
}).then(res => res.body);
|
|
},
|
|
removeFileInternal(refreshedToken, id, ifNotTooLate = cb => res => cb(res)) {
|
|
return Promise.resolve()
|
|
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
|
.then(ifNotTooLate(() => this.request(refreshedToken, {
|
|
method: 'DELETE',
|
|
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
|
params: {
|
|
supportsTeamDrives: true,
|
|
},
|
|
})));
|
|
},
|
|
getFileRevisionsInternal(refreshedToken, id) {
|
|
return Promise.resolve()
|
|
.then(() => {
|
|
const revisions = [];
|
|
const getPage = pageToken => this.request(refreshedToken, {
|
|
method: 'GET',
|
|
url: `https://www.googleapis.com/drive/v3/files/${id}/revisions`,
|
|
params: {
|
|
pageToken,
|
|
pageSize: 1000,
|
|
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
|
|
},
|
|
})
|
|
.then((res) => {
|
|
res.body.revisions.forEach((revision) => {
|
|
store.commit('userInfo/addItem', {
|
|
id: revision.lastModifyingUser.permissionId,
|
|
name: revision.lastModifyingUser.displayName,
|
|
imageUrl: revision.lastModifyingUser.photoLink,
|
|
});
|
|
revisions.push(revision);
|
|
});
|
|
if (res.body.nextPageToken) {
|
|
return getPage(res.body.nextPageToken);
|
|
}
|
|
return revisions;
|
|
});
|
|
|
|
return getPage();
|
|
});
|
|
},
|
|
downloadFileRevisionInternal(refreshedToken, fileId, revisionId) {
|
|
return Promise.resolve()
|
|
.then(() => this.request(refreshedToken, {
|
|
method: 'GET',
|
|
url: `https://www.googleapis.com/drive/v3/files/${fileId}/revisions/${revisionId}?alt=media`,
|
|
raw: true,
|
|
}).then(res => res.body));
|
|
},
|
|
getUser(userId) {
|
|
return networkSvc.request({
|
|
method: 'GET',
|
|
url: `https://www.googleapis.com/plus/v1/people/${userId}?key=${apiKey}`,
|
|
}, true)
|
|
.then((res) => {
|
|
store.commit('userInfo/addItem', {
|
|
id: res.body.id,
|
|
name: res.body.displayName,
|
|
imageUrl: (res.body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
|
});
|
|
return res.body;
|
|
});
|
|
},
|
|
startOauth2(scopes, sub = null, silent = false) {
|
|
return networkSvc.startOauth2(
|
|
'https://accounts.google.com/o/oauth2/v2/auth', {
|
|
client_id: clientId,
|
|
response_type: 'token id_token',
|
|
scope: ['openid', ...scopes].join(' '),
|
|
hd: appsDomain,
|
|
login_hint: sub,
|
|
prompt: silent ? 'none' : null,
|
|
nonce: utils.uid(),
|
|
}, silent)
|
|
// Call the token info endpoint
|
|
.then(data => networkSvc.request({
|
|
method: 'POST',
|
|
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
|
|
params: {
|
|
access_token: data.accessToken,
|
|
},
|
|
}, true).then((res) => {
|
|
// Check the returned client ID consistency
|
|
if (res.body.aud !== clientId) {
|
|
throw new Error('Client ID inconsistent.');
|
|
}
|
|
// Check the returned sub consistency
|
|
if (sub && `${res.body.sub}` !== sub) {
|
|
throw new Error('Google account ID not expected.');
|
|
}
|
|
// Build token object including scopes and sub
|
|
return {
|
|
scopes,
|
|
accessToken: data.accessToken,
|
|
expiresOn: Date.now() + (data.expiresIn * 1000),
|
|
idToken: data.idToken,
|
|
sub: `${res.body.sub}`,
|
|
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
|
|
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
|
isSponsor: false,
|
|
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
|
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
|
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
|
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
|
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
|
};
|
|
}))
|
|
// Call the user info endpoint
|
|
.then(token => this.getUser(token.sub)
|
|
.catch((err) => {
|
|
if (err.status === 404) {
|
|
store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo!');
|
|
} else {
|
|
throw err;
|
|
}
|
|
})
|
|
.then((user = {}) => {
|
|
const existingToken = store.getters['data/googleTokens'][token.sub];
|
|
// Add name to token
|
|
token.name = user.displayName || (existingToken && existingToken.name) || 'Unknown';
|
|
if (existingToken) {
|
|
// We probably retrieved a new token with restricted scopes.
|
|
// That's no problem, token will be refreshed later with merged scopes.
|
|
// Restore flags
|
|
Object.assign(token, {
|
|
isLogin: existingToken.isLogin || token.isLogin,
|
|
isSponsor: existingToken.isSponsor,
|
|
isDrive: existingToken.isDrive || token.isDrive,
|
|
isBlogger: existingToken.isBlogger || token.isBlogger,
|
|
isPhotos: existingToken.isPhotos || token.isPhotos,
|
|
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
|
|
});
|
|
}
|
|
return token.isLogin && networkSvc.request({
|
|
method: 'GET',
|
|
url: 'userInfo',
|
|
params: {
|
|
idToken: token.idToken,
|
|
},
|
|
})
|
|
.then((res) => {
|
|
token.isSponsor = res.body.sponsorUntil > Date.now();
|
|
}, () => {
|
|
// Ignore error
|
|
});
|
|
})
|
|
.then(() => {
|
|
// Add token to googleTokens
|
|
store.dispatch('data/setGoogleToken', token);
|
|
return token;
|
|
}));
|
|
},
|
|
refreshToken(token, scopes = []) {
|
|
const sub = token.sub;
|
|
const lastToken = store.getters['data/googleTokens'][sub];
|
|
const mergedScopes = [...new Set([
|
|
...scopes,
|
|
...lastToken.scopes,
|
|
])];
|
|
|
|
return Promise.resolve()
|
|
.then(() => {
|
|
if (
|
|
// If we already have permissions for the requested scopes
|
|
mergedScopes.length === lastToken.scopes.length &&
|
|
// And lastToken is not expired
|
|
lastToken.expiresOn > Date.now() + tokenExpirationMargin &&
|
|
// And in case of a login token, ID token is still valid
|
|
(!lastToken.isLogin || checkIdToken(lastToken.idToken))
|
|
) {
|
|
return lastToken;
|
|
}
|
|
// New scopes are requested or existing token is going to expire.
|
|
// Try to get a new token in background
|
|
return this.startOauth2(mergedScopes, sub, true)
|
|
// If it fails try to popup a window
|
|
.catch((err) => {
|
|
if (store.state.offline) {
|
|
throw err;
|
|
}
|
|
return store.dispatch('modal/providerRedirection', {
|
|
providerName: 'Google',
|
|
onResolve: () => this.startOauth2(mergedScopes, sub),
|
|
});
|
|
});
|
|
});
|
|
},
|
|
loadClientScript() {
|
|
if (gapi) {
|
|
return Promise.resolve();
|
|
}
|
|
return networkSvc.loadScript('https://apis.google.com/js/api.js')
|
|
.then(() => Promise.all(libraries.map(
|
|
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
|
callback: resolve,
|
|
onerror: reject,
|
|
timeout: 30000,
|
|
ontimeout: reject,
|
|
})))))
|
|
.then(() => {
|
|
gapi = window.gapi;
|
|
google = window.google;
|
|
});
|
|
},
|
|
getSponsorship(token) {
|
|
return this.refreshToken(token)
|
|
.then(refreshedToken => networkSvc.request({
|
|
method: 'GET',
|
|
url: 'userInfo',
|
|
params: {
|
|
idToken: refreshedToken.idToken,
|
|
},
|
|
}, true));
|
|
},
|
|
signin() {
|
|
return this.startOauth2(driveAppDataScopes);
|
|
},
|
|
addDriveAccount(fullAccess = false, sub = null) {
|
|
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
|
|
},
|
|
addBloggerAccount() {
|
|
return this.startOauth2(bloggerScopes);
|
|
},
|
|
addPhotosAccount() {
|
|
return this.startOauth2(photosScopes);
|
|
},
|
|
getChanges(token, startPageToken, isAppData, teamDriveId = null) {
|
|
const result = {
|
|
changes: [],
|
|
};
|
|
let fileFields = 'file/name';
|
|
if (!isAppData) {
|
|
fileFields += ',file/parents,file/mimeType,file/appProperties';
|
|
}
|
|
return this.refreshToken(token, isAppData ? driveAppDataScopes : getDriveScopes(token))
|
|
.then((refreshedToken) => {
|
|
const getPage = (pageToken = '1') => this.request(refreshedToken, {
|
|
method: 'GET',
|
|
url: 'https://www.googleapis.com/drive/v3/changes',
|
|
params: {
|
|
pageToken,
|
|
spaces: isAppData ? 'appDataFolder' : 'drive',
|
|
pageSize: 1000,
|
|
fields: `nextPageToken,newStartPageToken,changes(fileId,${fileFields})`,
|
|
supportsTeamDrives: true,
|
|
includeTeamDriveItems: !!teamDriveId,
|
|
teamDriveId,
|
|
},
|
|
})
|
|
.then((res) => {
|
|
result.changes = result.changes.concat(res.body.changes.filter(item => item.fileId));
|
|
if (res.body.nextPageToken) {
|
|
return getPage(res.body.nextPageToken);
|
|
}
|
|
result.startPageToken = res.body.newStartPageToken;
|
|
return result;
|
|
});
|
|
|
|
return getPage(startPageToken);
|
|
});
|
|
},
|
|
uploadFile(
|
|
token,
|
|
name,
|
|
parents,
|
|
appProperties,
|
|
media,
|
|
mediaType,
|
|
fileId,
|
|
oldParents,
|
|
ifNotTooLate,
|
|
) {
|
|
return this.refreshToken(token, getDriveScopes(token))
|
|
.then(refreshedToken => this.uploadFileInternal(
|
|
refreshedToken,
|
|
name,
|
|
parents,
|
|
appProperties,
|
|
media,
|
|
mediaType,
|
|
fileId,
|
|
oldParents,
|
|
ifNotTooLate,
|
|
));
|
|
},
|
|
uploadAppDataFile(token, name, media, fileId, ifNotTooLate) {
|
|
return this.refreshToken(token, driveAppDataScopes)
|
|
.then(refreshedToken => this.uploadFileInternal(
|
|
refreshedToken,
|
|
name,
|
|
['appDataFolder'],
|
|
undefined,
|
|
media,
|
|
undefined,
|
|
fileId,
|
|
undefined,
|
|
ifNotTooLate,
|
|
));
|
|
},
|
|
getFile(token, id) {
|
|
return this.refreshToken(token, getDriveScopes(token))
|
|
.then(refreshedToken => this.request(refreshedToken, {
|
|
method: 'GET',
|
|
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
|
params: {
|
|
fields: 'id,name,mimeType,appProperties,teamDriveId',
|
|
supportsTeamDrives: true,
|
|
},
|
|
})
|
|
.then(res => res.body));
|
|
},
|
|
downloadFile(token, id) {
|
|
return this.refreshToken(token, getDriveScopes(token))
|
|
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
|
},
|
|
downloadAppDataFile(token, id) {
|
|
return this.refreshToken(token, driveAppDataScopes)
|
|
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
|
},
|
|
removeFile(token, id, ifNotTooLate) {
|
|
return this.refreshToken(token, getDriveScopes(token))
|
|
.then(refreshedToken => this.removeFileInternal(refreshedToken, id, ifNotTooLate));
|
|
},
|
|
removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
|
|
return this.refreshToken(token, driveAppDataScopes)
|
|
.then(refreshedToken => this.removeFileInternal(refreshedToken, id, ifNotTooLate));
|
|
},
|
|
getFileRevisions(token, id) {
|
|
return this.refreshToken(token, getDriveScopes(token))
|
|
.then(refreshedToken => this.getFileRevisionsInternal(refreshedToken, id));
|
|
},
|
|
getAppDataFileRevisions(token, id) {
|
|
return this.refreshToken(token, driveAppDataScopes)
|
|
.then(refreshedToken => this.getFileRevisionsInternal(refreshedToken, id));
|
|
},
|
|
downloadFileRevision(token, fileId, revisionId) {
|
|
return this.refreshToken(token, getDriveScopes(token))
|
|
.then(refreshedToken => this.downloadFileRevisionInternal(
|
|
refreshedToken, fileId, revisionId));
|
|
},
|
|
downloadAppDataFileRevision(token, fileId, revisionId) {
|
|
return this.refreshToken(token, driveAppDataScopes)
|
|
.then(refreshedToken => this.downloadFileRevisionInternal(
|
|
refreshedToken, fileId, revisionId));
|
|
},
|
|
uploadBlogger(
|
|
token, blogUrl, blogId, postId, title, content, labels, isDraft, published, isPage,
|
|
) {
|
|
return this.refreshToken(token, bloggerScopes)
|
|
.then(refreshedToken => Promise.resolve()
|
|
.then(() => {
|
|
if (blogId) {
|
|
return blogId;
|
|
}
|
|
return this.request(refreshedToken, {
|
|
url: 'https://www.googleapis.com/blogger/v3/blogs/byurl',
|
|
params: {
|
|
url: blogUrl,
|
|
},
|
|
}).then(res => res.body.id);
|
|
})
|
|
.then((resolvedBlogId) => {
|
|
const path = isPage ? 'pages' : 'posts';
|
|
const options = {
|
|
method: 'POST',
|
|
url: `https://www.googleapis.com/blogger/v3/blogs/${resolvedBlogId}/${path}/`,
|
|
body: {
|
|
kind: isPage ? 'blogger#page' : 'blogger#post',
|
|
blog: {
|
|
id: resolvedBlogId,
|
|
},
|
|
title,
|
|
content,
|
|
},
|
|
};
|
|
if (labels) {
|
|
options.body.labels = labels;
|
|
}
|
|
if (published) {
|
|
options.body.published = published.toISOString();
|
|
}
|
|
// If it's an update
|
|
if (postId) {
|
|
options.method = 'PUT';
|
|
options.url += postId;
|
|
options.body.id = postId;
|
|
}
|
|
return this.request(refreshedToken, options)
|
|
.then(res => res.body);
|
|
})
|
|
.then((post) => {
|
|
if (isPage) {
|
|
return post;
|
|
}
|
|
const options = {
|
|
method: 'POST',
|
|
url: `https://www.googleapis.com/blogger/v3/blogs/${post.blog.id}/posts/${post.id}/`,
|
|
params: {},
|
|
};
|
|
if (isDraft) {
|
|
options.url += 'revert';
|
|
} else {
|
|
options.url += 'publish';
|
|
if (published) {
|
|
options.params.publishDate = published.toISOString();
|
|
}
|
|
}
|
|
return this.request(refreshedToken, options)
|
|
.then(res => res.body);
|
|
}));
|
|
},
|
|
openPicker(token, type = 'doc') {
|
|
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
|
|
return this.loadClientScript()
|
|
.then(() => this.refreshToken(token, scopes))
|
|
.then(refreshedToken => new Promise((resolve) => {
|
|
let picker;
|
|
const pickerBuilder = new google.picker.PickerBuilder()
|
|
.setOAuthToken(refreshedToken.accessToken)
|
|
.enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
|
|
.hideTitleBar()
|
|
.setCallback((data) => {
|
|
switch (data[google.picker.Response.ACTION]) {
|
|
case google.picker.Action.PICKED:
|
|
case google.picker.Action.CANCEL:
|
|
resolve(data.docs || []);
|
|
picker.dispose();
|
|
break;
|
|
default:
|
|
}
|
|
});
|
|
switch (type) {
|
|
default:
|
|
case 'doc': {
|
|
const mimeTypes = [
|
|
'text/plain',
|
|
'text/x-markdown',
|
|
'application/octet-stream',
|
|
].join(',');
|
|
|
|
const view = new google.picker.DocsView(google.picker.ViewId.DOCS);
|
|
view.setMimeTypes(mimeTypes);
|
|
pickerBuilder.addView(view);
|
|
|
|
const teamDriveView = new google.picker.DocsView(google.picker.ViewId.DOCS);
|
|
teamDriveView.setMimeTypes(mimeTypes);
|
|
teamDriveView.setEnableTeamDrives(true);
|
|
pickerBuilder.addView(teamDriveView);
|
|
|
|
pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
|
|
pickerBuilder.enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES);
|
|
break;
|
|
}
|
|
case 'folder': {
|
|
const folderView = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
|
|
folderView.setSelectFolderEnabled(true);
|
|
folderView.setMimeTypes(this.folderMimeType);
|
|
pickerBuilder.addView(folderView);
|
|
|
|
const teamDriveView = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
|
|
teamDriveView.setSelectFolderEnabled(true);
|
|
teamDriveView.setEnableTeamDrives(true);
|
|
teamDriveView.setMimeTypes(this.folderMimeType);
|
|
pickerBuilder.addView(teamDriveView);
|
|
break;
|
|
}
|
|
case 'img': {
|
|
const view = new google.picker.PhotosView();
|
|
view.setType('highlights');
|
|
pickerBuilder.addView(view);
|
|
pickerBuilder.addView(google.picker.ViewId.PHOTO_UPLOAD);
|
|
break;
|
|
}
|
|
}
|
|
picker = pickerBuilder.build();
|
|
picker.setVisible(true);
|
|
}));
|
|
},
|
|
};
|