Наконец, с большим количеством проб и ошибок, мне удалось сделать это чисто REST способом. Я удалил все библиотеки Google, кроме "com.google.android.gms:play-services-auth
", поскольку он предоставляет пользователям функциональность, позволяющую моему приложению получать доступ к области действия Google Диска.
Здесь я покажу простой класс CloudServiceImpl
, который может записывать резервную копию на Google Drive и восстанавливать из последней созданной резервной копии. Если вам необходимо восстановить из определенной резервной копии, не стесняйтесь изменить ее:
.........................
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
класс позволяет переопределить CloudServiceImpl
в разных вариантах:
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 MainActivity 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);
}
}
В вашей деятельности:
@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);
}
Этот пример довольно многословен. Но он добавляет <20 методов, по сравнению с 10 тыс. <br>
Также вам необходимо добавить в ваш проект strings.xml default_web_client_id
и client_secret
. Вы найдете его в консоли API Google, но на этот раз используйте «веб-клиент (автоматически созданный службой Google)», а не идентификатор клиента, который вы использовали для старого Google Drive API.