Development:Android Google Drive backup functionality without so much dependencies

From Olekdia Wiki
Revision as of 06:35, 17 January 2019 by Oleksandr (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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 override CloudServiceImpl 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 and client_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.