Development:Android Google Drive backup functionality without so much dependencies
From Olekdia Wiki
This time we are forced to implement new API because of deprecation of Google Drive Android API. Android libraries for Google Drive REST API add so much crap into your project:
implementation 'com.google.android.gms:play-services-auth:16.0.1' implementation 'com.google.http-client:google-http-client-gson:1.26.0' implementation('com.google.api-client:google-api-client-android:1.26.0') { exclude group: 'org.apache.httpcomponents' } implementation('com.google.apis:google-api-services-drive:v3-rev136-1.25.0') { exclude group: 'org.apache.httpcomponents' }
Every library has own dependencies, and it will add 10k methods to your project, and it is with Proguard minification enabled.
I have managed to implement simple Backup/Restore functionality with more RESTy way. I have removed all google libraries except "com.google.android.gms:play-services-auth
", as it provides a functionality, for users to allow my app to access the Google Drive scope.
- Here I will show simple
CloudServiceImpl
class which can write backup to Google Drive, and restore from the last created backup. If you need to restore from specific backup, feel free to modify it:
......................... import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.common.api.Scope; import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; public class CloudServiceImpl implements OnSuccessListener<GoogleSignInAccount>, OnFailureListener { private static final String LINE_FEED = "\r\n"; private static final String APP_FOLDER_ID = "appDataFolder"; private static final String SCOPE_APPDATA = "https://www.googleapis.com/auth/drive.appdata"; private static final String FILES_REST_URL = "https://www.googleapis.com/drive/v3/files"; private static final String AUTH_REST_URL = "https://www.googleapis.com/oauth2/v4/token"; private static final String AUTHORIZATION_PARAM = "Authorization"; private static final String BEARER_VAL = "Bearer "; private static final String CONTENT_TYPE_PARAM = "Content-Type: "; private static final String DB_NAME = "prana_breath.sqlite"; private static final String SQLITE_MIME = "application/x-sqlite3"; private Activity mActivity; private int mNextGoogleApiOperation = INVALID; private String mAccessToken; private long mTokenExpired; private String mAuthCode; public CloudServiceImpl(final Activity activity) { mActivity = activity; } public final void disconnect() { mActivity = null; mNextGoogleApiOperation = INVALID; mAuthCode = null; mAccessToken = null; mTokenExpired = 0; } public final void connectAndStartOperation(final int nextOperation) { mNextGoogleApiOperation = nextOperation; onChangeProgressBarVisibility(View.VISIBLE); if (mAuthCode == null) { final GoogleSignInOptions signInOptions = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestEmail() .requestScopes(new Scope(SCOPE_APPDATA)) .requestServerAuthCode(getString(R.string.default_web_client_id)) .build(); final GoogleSignInClient client = GoogleSignIn.getClient(mActivity, signInOptions); mActivity.startActivityForResult(client.getSignInIntent(), RequestCode.CLOUD_RESOLUTION); } else { onGoogleDriveConnected(mNextGoogleApiOperation); mNextGoogleApiOperation = INVALID; } } public final void handleActivityResult(final int requestCode, final Intent data) { if (requestCode == RequestCode.CLOUD_RESOLUTION) { GoogleSignIn.getSignedInAccountFromIntent(data) .addOnSuccessListener(this) .addOnFailureListener(this); } } //-------------------------------------------------------------------------------------------------- // Event handlers //-------------------------------------------------------------------------------------------------- @Override public void onSuccess(GoogleSignInAccount googleAccount) { mAuthCode = googleAccount.getServerAuthCode(); // DebugHelper.log("getServerAuthCode:", googleAccount.getServerAuthCode()); onChangeProgressBarVisibility(View.GONE); onChangeProgressDlgVisibility(View.VISIBLE); onGoogleDriveConnected(mNextGoogleApiOperation); mNextGoogleApiOperation = INVALID; } @Override public void onFailure(@NonNull Exception e) { onChangeProgressBarVisibility(View.GONE); onChangeProgressDlgVisibility(View.GONE); mNextGoogleApiOperation = INVALID; ToastHelper.showToastSafe(getString(R.string.error_toast) + ": " + e.getMessage()); } private void onGoogleDriveConnected(final int operation) { switch (operation) { case CloudHelper.BACKUP_CODE: onBackupToDriveAsync(); break; case CloudHelper.RESTORE_CODE: onRestoreFromDriveAsync(); break; } } //-------------------------------------------------------------------------------------------------- // Private methods //-------------------------------------------------------------------------------------------------- private boolean isRequestInvalid() { return mActivity == null; } @SuppressLint("StaticFieldLeak") private void onBackupToDriveAsync() { final AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... parameters) { BackupDelegate.backupPrefs(); // Here you could write your preferences to the database (Remove it if not needed) writeDbToDrive(); return null; } @Override protected void onPostExecute(Void aVoid) { onChangeProgressDlgVisibility(View.GONE); onChangeProgressBarVisibility(View.GONE); } }; asyncTask.execute(); } @SuppressLint("StaticFieldLeak") private void onRestoreFromDriveAsync() { final AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... parameters) { readDbFromDrive(); return null; } @Override protected void onPostExecute(Void aVoid) { onChangeProgressDlgVisibility(View.GONE); onChangeProgressBarVisibility(View.GONE); } }; asyncTask.execute(); } /** * https://developers.google.com/drive/api/v3/multipart-upload */ private void writeDbToDrive() { HttpURLConnection conn = null; OutputStream os = null; final String accessToken = requestAccessToken(); if (accessToken == null || isRequestInvalid()) return; try { final String boundary = "pb" + System.currentTimeMillis(); final URL url = new URL("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setUseCaches(false); conn.setDoOutput(true); conn.setDoInput(true); conn.setConnectTimeout(5000); conn.setRequestProperty(AUTHORIZATION_PARAM, BEARER_VAL + accessToken); conn.setRequestProperty("Content-Type", "multipart/related; boundary=" + boundary); /////// Prepare data final String timestamp = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.US).format(new Date()); // Prepare file metadata (Change your backup file name here) final StringBuilder b = new StringBuilder(); b.append('{') .append("\"name\":").append('\"').append("prana_breath_").append(timestamp).append(".db").append('\"').append(',') .append("\"mimeType\":").append("\"application\\/x-sqlite3\"").append(',') .append("\"parents\":").append("[\"").append(APP_FOLDER_ID).append("\"]") .append('}'); final String metadata = b.toString(); final byte[] data = readFile(getAppDbFile()); /////// Calculate body length int bodyLength = 0; // MetaData part b.setLength(0); b.append("--").append(boundary).append(LINE_FEED); b.append(CONTENT_TYPE_PARAM).append("application/json; charset=UTF-8").append(LINE_FEED); b.append(LINE_FEED); b.append(metadata).append(LINE_FEED); b.append(LINE_FEED); b.append("--").append(boundary).append(LINE_FEED); b.append(CONTENT_TYPE_PARAM).append(SQLITE_MIME).append(LINE_FEED); b.append(LINE_FEED); final byte[] beforeFilePart = b.toString().getBytes("UTF_8"); bodyLength += beforeFilePart.length; bodyLength += data.length; // File b.setLength(0); b.append(LINE_FEED); b.append("--").append(boundary).append("--"); final byte[] afterFilePart = b.toString().getBytes("UTF_8"); bodyLength += afterFilePart.length; conn.setRequestProperty("Content-Length", String.valueOf(bodyLength)); if (BuildConfig.DEBUG_MODE) DebugHelper.log("LENGTH", bodyLength); /////// Write to socket os = conn.getOutputStream(); os.write(beforeFilePart); os.write(data); os.write(afterFilePart); os.flush(); final String msg = conn.getResponseMessage(); final int code = conn.getResponseCode(); if (code == 200) { ToastHelper.showToastSafe(R.string.backup_success_toast); } else { ToastHelper.showToastSafe(getString(R.string.error_toast) + ": " + msg); } } catch (Exception e) { e.printStackTrace(); ToastHelper.showToastSafe(e.getMessage()); } finally { if (os != null) { try { os.close(); } catch (IOException e) { } } if (conn != null) { conn.disconnect(); } } } /** * https://developers.google.com/drive/api/v3/manage-downloads */ private void readDbFromDrive() { if (isRequestInvalid()) return; HttpURLConnection conn = null; InputStream is = null; final String accessToken = requestAccessToken(); if (accessToken == null || isRequestInvalid()) return; try { final String dbFileId = getLatestDbFileIdOnDrive(); if (isRequestInvalid()) return; if (dbFileId == null || dbFileId.length() == 0 || dbFileId.equals(NULL_STR)) { return; } final String request = FILES_REST_URL + '/' + dbFileId + "?alt=media"; final URL url = new URL(request); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setUseCaches(false); conn.setDoInput(true); conn.setConnectTimeout(5000); conn.setRequestProperty(AUTHORIZATION_PARAM, BEARER_VAL + accessToken); is = conn.getInputStream(); if (restoreDbFromDrive(is)) BackupDelegate.totalRefreshAfterRestore(); } catch (Exception e) { ToastHelper.showToastSafe(e.getMessage()); } finally { if (is != null) { try { is.close(); } catch (IOException e) { } } if (conn != null) { conn.disconnect(); } } } /** * https://developers.google.com/drive/api/v3/reference/files/list * @return */ private final String getLatestDbFileIdOnDrive() { HttpURLConnection conn = null; InputStream is = null; InputStreamReader isr = null; BufferedReader br = null; try { final StringBuilder b = new StringBuilder(); b.append(FILES_REST_URL).append('?') .append("spaces=").append(APP_FOLDER_ID).append('&') .append("orderBy=").append(URLEncoder.encode("createdTime desc", "UTF_8")).append('&') .append("pageSize=").append("2"); final URL url = new URL(b.toString()); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setUseCaches(false); conn.setDoInput(true); conn.setConnectTimeout(5000); conn.setRequestProperty(AUTHORIZATION_PARAM, BEARER_VAL + mAccessToken); final int responseCode = conn.getResponseCode(); if (200 <= responseCode && responseCode <= 299) { is = conn.getInputStream(); isr = new InputStreamReader(is); br = new BufferedReader(isr); } else { ToastHelper.showToastSafe(conn.getResponseMessage()); return null; /*is = conn.getErrorStream(); isr = new InputStreamReader(is); br = new BufferedReader(isr);*/ } b.setLength(0); String output; while ((output = br.readLine()) != null) { b.append(output); } final JSONObject jsonResponse = new JSONObject(b.toString()); final JSONArray files = jsonResponse.getJSONArray("files"); if (files.length() == 0) { ToastHelper.showToastSafe(R.string.no_backup_toast); return null; } final JSONObject file = files.getJSONObject(0); return file.getString("id"); } catch (Exception e) { ToastHelper.showToastSafe(e.getMessage()); } finally { if (is != null) { try { is.close(); } catch (IOException e) { } } if (isr != null) { try { isr.close(); } catch (IOException e) { } } if (br != null) { try { br.close(); } catch (IOException e) { } } if (conn != null) { conn.disconnect(); } } return null; } /** * https://developers.google.com/identity/protocols/OAuth2WebServer#exchange-authorization-code * */ private String requestAccessToken() { if (mAccessToken != null && SystemClock.elapsedRealtime() < mTokenExpired) return mAccessToken; mTokenExpired = 0; mAccessToken = null; HttpURLConnection conn = null; OutputStream os = null; InputStream is = null; InputStreamReader isr = null; BufferedReader br = null; try { final URL url = new URL(AUTH_REST_URL); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setUseCaches(false); conn.setDoInput(true); conn.setDoOutput(true); conn.setConnectTimeout(3000); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); final StringBuilder b = new StringBuilder(); b.append("code=").append(mAuthCode).append('&') .append("client_id=").append(getString(R.string.default_web_client_id)).append('&') .append("client_secret=").append(getString(R.string.client_secret)).append('&') .append("redirect_uri=").append("").append('&') .append("grant_type=").append("authorization_code"); final byte[] postData = b.toString().getBytes("UTF_8"); os = conn.getOutputStream(); os.write(postData); final int responseCode = conn.getResponseCode(); if (200 <= responseCode && responseCode <= 299) { is = conn.getInputStream(); isr = new InputStreamReader(is); br = new BufferedReader(isr); } else { ToastHelper.showToastSafe(conn.getResponseMessage()); return null; } b.setLength(0); String output; while ((output = br.readLine()) != null) { b.append(output); } final JSONObject jsonResponse = new JSONObject(b.toString()); mAccessToken = jsonResponse.getString("access_token"); mTokenExpired = SystemClock.elapsedRealtime() + jsonResponse.getLong("expires_in") * 1000; return mAccessToken; } catch (Exception e) { ToastHelper.showToastSafe(e.getMessage()); } finally { if (os != null) { try { os.close(); } catch (IOException e) { } } if (is != null) { try { is.close(); } catch (IOException e) { } } if (isr != null) { try { isr.close(); } catch (IOException e) { } } if (br != null) { try { br.close(); } catch (IOException e) { } } if (conn != null) { conn.disconnect(); } } return null; } private boolean restoreDbFromDrive(final InputStream src) throws IOException { if (src == null) { ToastHelper.showToastSafe(R.string.no_backup_toast); } else { DbOpenHelper.getInstance().close(); // It is your SQLiteOpenHelper implementation (Close db before replacing it) writeStreamToFileOutput(src, new FileOutputStream(getAppDbFile())); return true; } return false; } private static byte[] readFile(File file) throws IOException { RandomAccessFile f = new RandomAccessFile(file, "r"); try { long longlength = f.length(); int length = (int) longlength; if (length != longlength) throw new IOException("File size >= 10 Mb"); byte[] data = new byte[length]; f.readFully(data); return data; } finally { f.close(); } } public static void writeStreamToFileOutput(final InputStream src, final FileOutputStream dst) throws IOException { try { final byte[] buffer = new byte[4 * 1024]; // or other buffer size int read; while ((read = src.read(buffer)) != -1) { dst.write(buffer, 0, read); } dst.flush(); } finally { src.close(); dst.close(); } } private static File getAppDbFile() { return mActivity.getApplicationContext().getDatabasePath(DB_NAME); } }
CloudHelper
class allows to overrideCloudServiceImpl
in different flavors:
public class CloudHelper { public static final BACKUP_CODE = 1; public static final RESTORE_CODE = 2; @Nullable private static CloudServiceImpl sCloudServiceImpl; public static void connectAndStartOperation(final Activity activity, final int nextOperation) { if (sCloudServiceImpl == null) { sCloudServiceImpl = new CloudServiceImpl(activity); } sCloudServiceImpl.connectAndStartOperation(nextOperation); } public static void disconnect() { if (sCloudServiceImpl != null) { sCloudServiceImpl.disconnect(); sCloudServiceImpl = null; } } public static void handleActivityResult(final int requestCode, final Intent data) { if (sCloudServiceImpl != null) sCloudServiceImpl.handleActivityResult(requestCode, data); } }
- In your Activity:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); CloudHelper.handleActivityResult(requestCode, data); } @Override protected void onDestroy() { CloudHelper.disconnect(); super.onDestroy(); } public void onBackupClick() { CloudHelper.connectAndStartOperation(CloudHelper.BACKUP_CODE); } public void onRestoreClick() { CloudHelper.connectAndStartOperation(CloudHelper.RESTORE_CODE); }
- This example quite verbose. But it adds < 20 methods, comparing to 10k.
- Also you need to add to your project strings.xml
default_web_client_id
andclient_secret
. You will find it in Google API Console, but this time use "Web client (auto created by Google Service)", not the client id that you have used for old Google Drive API.