我正在寻找与使用Android应用访问Google云端硬盘相关的一些指导.
1)我需要能够读取我的应用程序之外的用户上传的文件.这是否意味着我需要全驱动访问?(如果应用程序可以创建一个文件夹,然后查看该文件夹中存在的用户上传的所有文件,那就太棒了,但我不认为这样做.)
2)如果我需要全驱动访问权限,似乎Googles"Drive API for Android"不支持此功能,我需要使用REST API.我认为这是真的.
3)我需要来自Google的Auth 2.0客户端ID.如果我使用其余的API,这是否意味着我需要使用"Web应用程序"ID?我想我需要这个,因为我想要一个"授权代码".我无法使用"Android"类型ID.
4)我目前正在使用Android的"Google登录"来处理登录并提供身份验证代码.然后我可以将它转换为令牌+刷新令牌,并保存这些,这样我可以在一小时后以某种方式获得新的令牌.是否需要手动处理刷新令牌?
它变得丑陋,但我认为,因为我需要(?)全驱动访问,所以这是程序.
谢谢你的指导.
编辑:该问题已被确定为重复.提供的链接为问题#2提供了答案,但没有解决其他问题.
我同意这个问题很混乱......
我在回答我自己的问题.
我为此苦苦挣扎,因为A)Google的REST示例使用过时的登录过程,B)"登录"示例使用的代码不能与"完全访问"范围一起使用,而C)有太多不同的代码试图将它们放在一起的例子.
我现在看到它可以快速回答我的问题:1)是的,需要完全驱动器访问才能读取在我的应用程序之外上传的文件.2)是的,我需要使用REST api.3)是的,我需要一个"Web应用程序"客户端ID.4)Google登录似乎是目前登录的最佳方式,只要您保留刷新令牌,使用GoogleCredential对象和Drive api abject就会自动处理令牌刷新.
如果其他人正在努力使用最新的"登录"程序和REST v3从Android访问完全访问驱动器,下面是我的示例代码.
除了"Web应用程序"OAuth客户端ID之外,您还需要创建具有匹配包名称和证书指纹的"Android"类型ID,以便登录工作.另请注意,您的开发和生产版本将拥有不同的证书.来自这些Android客户端的ID /代码无需输入到应用程序中.
build.gradle:app
// Google Sign In compile 'com.google.android.gms:play-services-auth:10.0.1' // Drive REST API compile('com.google.apis:google-api-services-drive:v3-rev54-1.22.0') { exclude group: 'org.apache.httpcomponents' }
活动
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // Callback from Signin (Auth.GoogleSignInApi.getSignInIntent) if (requestCode == 1) { GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data); _googleApi.handleSignInResult(result); } }
一个"GoogleApi"类来完成这项工作
import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.os.Handler; import android.util.Log; import com.google.android.gms.auth.api.Auth; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.auth.api.signin.GoogleSignInResult; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Scope; import com.google.android.gms.common.api.Status; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.services.drive.Drive; import com.google.api.services.drive.model.File; import com.google.api.services.drive.model.FileList; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class GoogleApi implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { private Context _context; private Handler _handler; private GoogleCredential _credential; private Drive _drive; private GoogleApiClient _googleApiClient; // only set during login process private Activity _activity; // launch intent for login (UI) // Saved to data store private boolean _loggedIn; private String _refreshToken; // store, even if user is logged out as we may need to reuse private static final String ClientID = "xxxxxx.apps.googleusercontent.com"; // web client private static final String ClientSecret = "xxxxx"; // web client private class FileAndErrorMsg { public File file; public String errorMsg; public FileAndErrorMsg (File file_, String errorMsg_) { file = file_; errorMsg = errorMsg_; } } private class FileListAndErrorMsg { public ListfileList; public String errorMsg; public FileListAndErrorMsg (List fileList_, String errorMsg_) { fileList = fileList_; errorMsg = errorMsg_; } } // ------------------- // Constructor // ------------------- public GoogleApi (Context context) { _context = context; _handler = new Handler(); loadFromPrefs(); // loggedIn, refreshToken // create credential; will refresh itself automatically (in Drive calls) as long as valid refresh token exists HttpTransport transport = new NetHttpTransport(); JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); _credential = new GoogleCredential.Builder() .setTransport(transport) .setJsonFactory(jsonFactory) .setClientSecrets(ClientID, ClientSecret) // .addRefreshListener .build(); _credential.setRefreshToken(_refreshToken); // Get app name from Manifest (for Drive builder) ApplicationInfo appInfo = context.getApplicationInfo(); String appName = appInfo.labelRes == 0 ? appInfo.nonLocalizedLabel.toString() : context.getString(appInfo.labelRes); _drive = new Drive.Builder(transport, jsonFactory, _credential).setApplicationName(appName).build(); } // ------------------- // Auth // ------------------- // https://developers.google.com/identity/sign-in/android/offline-access#before_you_begin // https://developers.google.com/identity/sign-in/android/offline-access#enable_server-side_api_access_for_your_app // https://android-developers.googleblog.com/2016/02/using-credentials-between-your-server.html // https://android-developers.googleblog.com/2016/05/improving-security-and-user-experience.html public boolean isLoggedIn () { return _loggedIn; } public void startAuth(Activity activity) { startAuth(activity, false); } public void startAuth(Activity activity, boolean forceRefreshToken) { _activity = activity; _loggedIn = false; saveToPrefs(); GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestScopes(new Scope("https://www.googleapis.com/auth/drive")) .requestServerAuthCode(ClientID, forceRefreshToken) // if force, guaranteed to get back refresh token, but will show "offline access?" if Google already issued refresh token .build(); _googleApiClient = new GoogleApiClient.Builder(activity) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(Auth.GOOGLE_SIGN_IN_API, gso) .build(); _googleApiClient.connect(); } @Override public void onConnected(Bundle connectionHint) { // Called soon after .connect() // This is only called when starting our Login process. Sign Out first so select-account screen shown. (OK if not already signed in) Auth.GoogleSignInApi.signOut(_googleApiClient).setResultCallback(new ResultCallback () { @Override public void onResult(Status status) { // Start sign in Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(_googleApiClient); _activity.startActivityForResult(signInIntent, 1); // Activity's onActivityResult will use the same code: 1 } }); } @Override public void onConnectionSuspended(int cause) { authDone("Connection suspended."); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { authDone("Connection failed."); } public void handleSignInResult(GoogleSignInResult result) { // Callback from Activity > onActivityResult if (result.isSuccess()) { GoogleSignInAccount acct = result.getSignInAccount(); String authCode = acct.getServerAuthCode(); new Thread(new ContinueAuthWithAuthCode_Background(authCode)).start(); } else authDone("Login canceled or unable to connect to Google."); // can we get better error message? } private class ContinueAuthWithAuthCode_Background implements Runnable { String _authCode; public ContinueAuthWithAuthCode_Background (String authCode) { _authCode = authCode; } public void run() { // Convert authCode to tokens GoogleTokenResponse tokenResponse = null; String errorMsg = null; try { tokenResponse = new GoogleAuthorizationCodeTokenRequest(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), "https://www.googleapis.com/oauth2/v4/token", ClientID, ClientSecret, _authCode, "").execute(); } catch (IOException e) { errorMsg = e.getLocalizedMessage(); } final GoogleTokenResponse tokenResponseFinal = tokenResponse; final String errorMsgFinal = errorMsg; _handler.post(new Runnable() { public void run() { // Main thread GoogleTokenResponse tokenResponse = tokenResponseFinal; String errorMsg = errorMsgFinal; if (tokenResponse != null && errorMsg == null) { _credential.setFromTokenResponse(tokenResponse); // this will keep old refresh token if no new one sent _refreshToken = _credential.getRefreshToken(); _loggedIn = true; saveToPrefs(); // FIXME: if our refresh token is bad and we're not getting a new one, how do we deal with this? Log("New refresh token: " + tokenResponse.getRefreshToken()); } else if (errorMsg == null) errorMsg = "Get token error."; // shouldn't get here authDone(errorMsg); } }); } } private void authDone(String errorMsg) { // Disconnect (we only need googleApiClient for login process) if (_googleApiClient != null && _googleApiClient.isConnected()) _googleApiClient.disconnect(); _googleApiClient = null; } /* public void signOut() { Auth.GoogleSignInApi.signOut(_googleApiClient).setResultCallback(new ResultCallback () { @Override public void onResult(Status status) { } }); } public void revokeAccess() { // FIXME: I don't know yet, but this may revoke access for all android devices Auth.GoogleSignInApi.revokeAccess(_googleApiClient).setResultCallback(new ResultCallback () { @Override public void onResult(Status status) { } }); } */ public void LogOut() { _loggedIn = false; saveToPrefs(); // don't clear refresh token as we may need again } // ------------------- // API Calls // ------------------- public void makeApiCall() { new Thread(new TestApiCall_Background()).start(); } private class TestApiCall_Background implements Runnable { public void run() { FileAndErrorMsg fileAndErr = getFolderFromName_b("Many Files", null); if (fileAndErr.errorMsg != null) Log("getFolderFromName_b error: " + fileAndErr.errorMsg); else { FileListAndErrorMsg fileListAndErr = getFileListInFolder_b(fileAndErr.file); if (fileListAndErr.errorMsg != null) Log("getFileListInFolder_b error: " + fileListAndErr.errorMsg); else { Log("file count: " + fileListAndErr.fileList.size()); for (File file : fileListAndErr.fileList) { //Log(file.getName()); } } } _handler.post(new Runnable() { public void run() { // Main thread } }); } } private FileAndErrorMsg getFolderFromName_b (String folderName, File parent) { // parent can be null for top level // Working with folders: https://developers.google.com/drive/v3/web/folder File folder = null; folderName = folderName.replace("'", "\\'"); // escape ' String q = String.format(Locale.US, "mimeType='application/vnd.google-apps.folder' and '%s' in parents and name='%s' and trashed=false", parent == null ? "root" : parent.getId(), folderName); String errorMsg = null; try { FileList result = _drive.files().list().setQ(q).setPageSize(1000).execute(); int foundCount = 0; for (File file : result.getFiles()) { foundCount++; folder = file; } if (foundCount == 0) errorMsg = "Folder not found: " + folderName; else if (foundCount > 1) errorMsg = "More than one folder found with name (" + foundCount + "): " + folderName; } catch (IOException e) { errorMsg = e.getLocalizedMessage(); } if (errorMsg != null) folder = null; return new FileAndErrorMsg(folder, errorMsg); } private FileListAndErrorMsg getFileListInFolder_b (File folder) { // folder can be null for top level; does not return subfolder names List fileList = new ArrayList (); String q = String.format(Locale.US, "mimeType != 'application/vnd.google-apps.folder' and '%s' in parents and trashed=false", folder == null ? "root" : folder.getId()); String errorMsg = null; try { String pageToken = null; do { FileList result = _drive.files().list().setQ(q).setPageSize(1000).setPageToken(pageToken).execute(); fileList.addAll(result.getFiles()); pageToken = result.getNextPageToken(); } while (pageToken != null); } catch (IOException e) { errorMsg = e.getLocalizedMessage(); } if (errorMsg != null) fileList = null; return new FileListAndErrorMsg(fileList, errorMsg); } // ------------------- // Misc // ------------------- private void Log(String msg) { Log.v("ept", msg); } // ------------------- // Load/Save Tokens // ------------------- private void loadFromPrefs() { SharedPreferences pref = _context.getSharedPreferences("prefs", Context.MODE_PRIVATE); _loggedIn = pref.getBoolean("GoogleLoggedIn", false); _refreshToken = pref.getString("GoogleRefreshToken", null); } private void saveToPrefs() { SharedPreferences.Editor editor = _context.getSharedPreferences("prefs", Context.MODE_PRIVATE).edit(); editor.putBoolean("GoogleLoggedIn", _loggedIn); editor.putString("GoogleRefreshToken", _refreshToken); editor.apply(); // async } }