diff --git a/app/build.gradle b/app/build.gradle index f989e31..4c2a881 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,14 +50,11 @@ dependencies { compile 'com.android.support:support-v4:23.2.1' compile 'com.android.support:appcompat-v7:23.2.1' compile 'com.android.support:design:23.2.1' - // View annotation bindings - compile 'com.jakewharton:butterknife:7.0.1' - // Dependency injection tool - compile 'com.google.dagger:dagger:2.0.1' - // Charts - compile 'com.github.PhilJay:MPAndroidChart:v2.2.4' - // Advanced logging tool - compile 'com.jakewharton.timber:timber:4.1.2' + + compile 'com.jakewharton:butterknife:7.0.1' // View annotation bindings + compile 'com.google.dagger:dagger:2.0.1' // Dependency injection tool + compile 'com.github.PhilJay:MPAndroidChart:v2.2.4' // Charts + compile 'com.jakewharton.timber:timber:4.1.2' // Advanced logging tool apt 'com.google.dagger:dagger-compiler:2.0.1' provided 'org.glassfish:javax.annotation:10.0-b28' diff --git a/app/libs/dropbox-android-sdk-1.6.3.jar b/app/libs/dropbox-android-sdk-1.6.3.jar new file mode 100644 index 0000000..1a0ee36 Binary files /dev/null and b/app/libs/dropbox-android-sdk-1.6.3.jar differ diff --git a/app/libs/json_simple-1.1.jar b/app/libs/json_simple-1.1.jar new file mode 100644 index 0000000..f395f41 Binary files /dev/null and b/app/libs/json_simple-1.1.jar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 78b87df..8dde910 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + + + + + + + + + + + dbApi; + + @Bind(R.id.btn_backup_now) + View btnBackupNow; + @Bind(R.id.list_view) + ListView listView; + + @Override + protected int getContentViewId() { + return R.layout.activity_backup; + } + + @Override + protected boolean initData() { + getAppComponent().inject(BackupActivity.this); + + AppKeyPair appKeys = new AppKeyPair(APP_KEY, APP_SECRET); + String accessToken = preferenceController.readDropboxAccessToken(); + + AndroidAuthSession session = new AndroidAuthSession(appKeys); + dbApi = new DropboxAPI<>(session); + if (accessToken == null) dbApi.getSession().startOAuth2Authentication(BackupActivity.this); + else { + dbApi.getSession().setOAuth2AccessToken(accessToken); + fetchBackups(); + } + + return super.initData(); + } + + @Override + protected void initViews() { + super.initViews(); + btnBackupNow.setEnabled(preferenceController.readDropboxAccessToken() != null); + } + + @Override + protected void onResume() { + super.onResume(); + + if (dbApi.getSession().authenticationSuccessful()) { + try { + // Required to complete auth, sets the access token on the session + dbApi.getSession().finishAuthentication(); + preferenceController.writeDropboxAccessToken(dbApi.getSession().getOAuth2AccessToken()); + btnBackupNow.setEnabled(true); + fetchBackups(); + } catch (IllegalStateException e) { + Timber.e("Error authenticating: %s", e.getMessage()); + } + } + } + + @OnClick(R.id.btn_backup_now) + public void backupNow() { + startProgress(); + backupController.makeBackup(dbApi, new BackupController.OnBackupListener() { + @Override + public void onBackupSuccess() { + Timber.d("Backup success."); + stopProgress(); + fetchBackups(); + } + + @Override + public void onBackupFailure(String reason) { + Timber.d("Backup failure."); + stopProgress(); + showToast(R.string.failed_create_backup); + + if (BackupController.OnBackupListener.ERROR_AUTHENTICATION.equals(reason)) logout(); + } + }); + } + + @OnItemClick(R.id.list_view) + public void restoreBackupClicked(int position) { + final String backupName = listView.getAdapter().getItem(position).toString(); + + AlertDialog.Builder builder = new AlertDialog.Builder(BackupActivity.this); + builder.setTitle(getString(R.string.warning)); + builder.setMessage(getString(R.string.want_erase_and_restore, backupName)); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + restoreBackup(backupName); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + + private void restoreBackup(final String backupName) { + startProgress(); + backupController.restoreBackup(dbApi, backupName, new BackupController.OnRestoreBackupListener() { + @Override + public void onRestoreSuccess() { + Timber.d("Restore success."); + stopProgress(); + + AlertDialog.Builder builder = new AlertDialog.Builder(BackupActivity.this); + builder.setTitle(getString(R.string.backup_is_restored)); + builder.setMessage(getString(R.string.backup_restored, backupName)); + builder.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + MtApp.get().buildAppComponent(); + setResult(RESULT_OK); + finish(); + } + }); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + + @Override + public void onRestoreFailure(String reason) { + Timber.d("Restore failure."); + stopProgress(); + showToast(R.string.failed_restore_backup); + + if (BackupController.OnRestoreBackupListener.ERROR_AUTHENTICATION.equals(reason)) + logout(); + } + }); + } + + private void fetchBackups() { + startProgress(); + backupController.fetchBackups(dbApi, new BackupController.OnFetchBackupListListener() { + @Override + public void onBackupsFetched(@NonNull List backupList) { + stopProgress(); + ArrayAdapter adapter = new ArrayAdapter<>(BackupActivity.this, + android.R.layout.simple_list_item_1, backupList); + listView.setAdapter(adapter); + } + }); + } + + private void logout() { + preferenceController.writeDropboxAccessToken(null); + dbApi.getSession().startOAuth2Authentication(BackupActivity.this); + btnBackupNow.setEnabled(false); + } +} diff --git a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/activity/record/MainActivity.java b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/activity/record/MainActivity.java index 9e235cf..5a2a875 100644 --- a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/activity/record/MainActivity.java +++ b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/activity/record/MainActivity.java @@ -41,7 +41,7 @@ public class MainActivity extends BaseDrawerActivity { @SuppressWarnings("unused") private static final String TAG = "MainActivity"; - private static final int REQUEST_ACTION_RECORD = 1; + private static final int REQUEST_ACTION_RECORD = 6; private List recordList; private Period period; @@ -158,6 +158,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { update(); break; + case REQUEST_BACKUP: + getAppComponent().inject(MainActivity.this); + update(); + break; + default: break; } diff --git a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/controller/BackupController.java b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/controller/BackupController.java new file mode 100644 index 0000000..9d0bddd --- /dev/null +++ b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/controller/BackupController.java @@ -0,0 +1,277 @@ +package com.blogspot.e_kanivets.moneytracker.controller; + +import android.os.AsyncTask; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.dropbox.client2.DropboxAPI; +import com.dropbox.client2.android.AndroidAuthSession; +import com.dropbox.client2.exception.DropboxException; +import com.dropbox.client2.exception.DropboxUnlinkedException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Controller class to encapsulate backup logic. + * Created on 8/10/16. + * + * @author Evgenii Kanivets + */ +public class BackupController { + private FormatController formatController; + private String filesDir; + + public BackupController(FormatController formatController, String filesDir) { + this.formatController = formatController; + this.filesDir = filesDir; + } + + public void makeBackup(@NonNull DropboxAPI dbApi, + @Nullable OnBackupListener listener) { + FileInputStream fileInputStream = readAppDb(); + long fileLength = readAppDbFileLength(); + if (fileInputStream == null) return; + + DropboxBackupAsyncTask asyncTask = new DropboxBackupAsyncTask(dbApi, + formatController.formatDateAndTime(System.currentTimeMillis()), + fileInputStream, fileLength, listener); + asyncTask.execute(); + } + + public void restoreBackup(@NonNull DropboxAPI dbApi, @NonNull String backupName, + @Nullable final OnRestoreBackupListener listener) { + final File file = new File(getRestoreFileName()); + FileOutputStream outputStream = null; + try { + outputStream = new FileOutputStream(file); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + if (outputStream == null) { + if (listener != null) listener.onRestoreFailure(null); + } else { + final FileOutputStream finalOutputStream = outputStream; + DropboxRestoreBackupAsyncTask asyncTask = new DropboxRestoreBackupAsyncTask(dbApi, + backupName, outputStream, new OnRestoreBackupListener() { + @Override + public void onRestoreSuccess() { + try { + finalOutputStream.close(); + } catch (IOException e) { + if (listener != null) listener.onRestoreFailure(null); + e.printStackTrace(); + } + + if (file.exists() && file.length() != 0) { + boolean renamed = file.renameTo(new File(getAppDbFileName())); + if (listener != null) { + if (renamed) listener.onRestoreSuccess(); + else listener.onRestoreFailure(null); + } + } + } + + @Override + public void onRestoreFailure(String reason) { + if (listener != null) listener.onRestoreFailure(reason); + } + }); + asyncTask.execute(); + } + } + + public void fetchBackups(@NonNull DropboxAPI dbApi, + @Nullable OnFetchBackupListListener listener) { + DropboxFetchBackupListAsyncTask asyncTask = new DropboxFetchBackupListAsyncTask(dbApi, listener); + asyncTask.execute(); + } + + @Nullable + private FileInputStream readAppDb() { + File dbFile = new File(getAppDbFileName()); + FileInputStream fis = null; + + try { + fis = new FileInputStream(dbFile); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + return fis; + } + + private long readAppDbFileLength() { + File dbFile = new File(getAppDbFileName()); + + if (dbFile.exists()) return dbFile.length(); + else return 0; + } + + @NonNull + private String getAppDbFileName() { + return filesDir + "/databases/database"; + } + + @NonNull + private String getRestoreFileName() { + return getAppDbFileName() + ".restore"; + } + + private class DropboxBackupAsyncTask extends AsyncTask { + private DropboxAPI dbApi; + private String fileName; + private FileInputStream fileInputStream; + private long fileLength; + + @Nullable + private OnBackupListener listener; + + public DropboxBackupAsyncTask(DropboxAPI dbApi, String fileName, + FileInputStream fileInputStream, long fileLength, + @Nullable OnBackupListener listener) { + this.dbApi = dbApi; + this.fileName = fileName; + this.fileInputStream = fileInputStream; + this.fileLength = fileLength; + this.listener = listener; + } + + @Override + protected String doInBackground(Void... params) { + DropboxAPI.Entry response = null; + try { + response = dbApi.putFile(fileName, fileInputStream, fileLength, null, null); + } catch (DropboxUnlinkedException e) { + e.printStackTrace(); + return OnBackupListener.ERROR_AUTHENTICATION; + } catch (DropboxException e) { + e.printStackTrace(); + } + + if (response == null) return null; + else return OnBackupListener.SUCCESS; + } + + @Override + protected void onPostExecute(String result) { + super.onPostExecute(result); + if (listener == null) return; + + if (OnBackupListener.SUCCESS.equals(result)) listener.onBackupSuccess(); + else listener.onBackupFailure(result); + } + } + + private class DropboxFetchBackupListAsyncTask extends AsyncTask, List> { + private DropboxAPI dbApi; + + @Nullable + private OnFetchBackupListListener listener; + + public DropboxFetchBackupListAsyncTask(DropboxAPI dbApi, + @Nullable OnFetchBackupListListener listener) { + this.dbApi = dbApi; + this.listener = listener; + } + + @Override + protected List doInBackground(Void... params) { + List entryList = new ArrayList<>(); + List backupList = new ArrayList<>(); + + try { + DropboxAPI.Entry entry = dbApi.metadata("/", -1, null, true, null); + entryList = entry.contents; + } catch (DropboxException e) { + e.printStackTrace(); + } + + for (DropboxAPI.Entry entry : entryList) { + backupList.add(entry.fileName()); + } + + return backupList; + } + + @Override + protected void onPostExecute(List backupList) { + super.onPostExecute(backupList); + if (listener == null) return; + + Collections.reverse(backupList); + listener.onBackupsFetched(backupList); + } + } + + private class DropboxRestoreBackupAsyncTask extends AsyncTask { + private DropboxAPI dbApi; + private String backupName; + private FileOutputStream outputStream; + + @Nullable + private OnRestoreBackupListener listener; + + public DropboxRestoreBackupAsyncTask(DropboxAPI dbApi, String backupName, + FileOutputStream outputStream, + @Nullable OnRestoreBackupListener listener) { + this.dbApi = dbApi; + this.backupName = backupName; + this.outputStream = outputStream; + this.listener = listener; + } + + @Override + protected String doInBackground(Void... params) { + DropboxAPI.DropboxFileInfo info = null; + try { + info = dbApi.getFile(backupName, null, outputStream, null); + } catch (DropboxUnlinkedException e) { + e.printStackTrace(); + return OnRestoreBackupListener.ERROR_AUTHENTICATION; + } catch (DropboxException e) { + e.printStackTrace(); + } + + if (info == null) return null; + else return OnBackupListener.SUCCESS; + } + + @Override + protected void onPostExecute(String result) { + super.onPostExecute(result); + if (listener == null) return; + + if (OnBackupListener.SUCCESS.equals(result)) listener.onRestoreSuccess(); + else listener.onRestoreFailure(result); + } + } + + public interface OnBackupListener { + String SUCCESS = "success"; + String ERROR_AUTHENTICATION = "error_authentication"; + + void onBackupSuccess(); + + void onBackupFailure(String reason); + } + + public interface OnFetchBackupListListener { + void onBackupsFetched(@NonNull List backupList); + } + + public interface OnRestoreBackupListener { + String ERROR_AUTHENTICATION = "error_authentication"; + + void onRestoreSuccess(); + + void onRestoreFailure(String reason); + } +} diff --git a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/controller/PreferenceController.java b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/controller/PreferenceController.java index c342ee7..a3130f3 100644 --- a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/controller/PreferenceController.java +++ b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/controller/PreferenceController.java @@ -21,6 +21,7 @@ public class PreferenceController { private static final String KEY_FIRST_TS = "key_first_ts"; private static final String KEY_LAST_TS = "key_last_ts"; private static final String KEY_PERIOD_TYPE = "key_period_type"; + private static final String KEY_DROPBOX_ACCESS_TOKEN = "key_dropbox_access_token"; private static final int RATE_PERIOD = 5; @@ -79,6 +80,14 @@ public void writePeriodType(String periodType) { editor.apply(); } + public void writeDropboxAccessToken(String accessToken) { + SharedPreferences preferences = getDefaultPrefs(); + SharedPreferences.Editor editor = preferences.edit(); + + editor.putString(KEY_DROPBOX_ACCESS_TOKEN, accessToken); + editor.apply(); + } + public long readDefaultAccountId() { String defaultAccountPref = context.getString(R.string.pref_default_account); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -115,6 +124,11 @@ public String readPeriodType() { return getDefaultPrefs().getString(KEY_PERIOD_TYPE, null); } + @Nullable + public String readDropboxAccessToken() { + return getDefaultPrefs().getString(KEY_DROPBOX_ACCESS_TOKEN, null); + } + private SharedPreferences getDefaultPrefs() { return context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE); } diff --git a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/di/AppComponent.java b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/di/AppComponent.java index aeac91e..665253e 100644 --- a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/di/AppComponent.java +++ b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/di/AppComponent.java @@ -1,6 +1,7 @@ package com.blogspot.e_kanivets.moneytracker.di; import com.blogspot.e_kanivets.moneytracker.activity.ChartsActivity; +import com.blogspot.e_kanivets.moneytracker.activity.external.BackupActivity; import com.blogspot.e_kanivets.moneytracker.activity.external.ExportActivity; import com.blogspot.e_kanivets.moneytracker.activity.external.ImportActivity; import com.blogspot.e_kanivets.moneytracker.activity.ReportActivity; @@ -58,6 +59,8 @@ public interface AppComponent { void inject(ChartsActivity chartsActivity); + void inject(BackupActivity backupActivity); + void inject(SettingsActivity.SettingsFragment settingsFragment); void inject(AccountsSummaryPresenter accountsSummaryPresenter); diff --git a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/di/module/ControllerModule.java b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/di/module/ControllerModule.java index 48163b5..d54f1be 100644 --- a/app/src/main/java/com/blogspot/e_kanivets/moneytracker/di/module/ControllerModule.java +++ b/app/src/main/java/com/blogspot/e_kanivets/moneytracker/di/module/ControllerModule.java @@ -3,6 +3,7 @@ import android.content.Context; import android.support.annotation.NonNull; +import com.blogspot.e_kanivets.moneytracker.controller.BackupController; import com.blogspot.e_kanivets.moneytracker.controller.external.ExportController; import com.blogspot.e_kanivets.moneytracker.controller.FormatController; import com.blogspot.e_kanivets.moneytracker.controller.PeriodController; @@ -122,4 +123,11 @@ public ExportController providesExportController(RecordController recordControll public ImportController providesImportController(RecordController recordController) { return new ImportController(recordController); } + + @Provides + @NonNull + @Singleton + public BackupController providesBackupController(FormatController formatController) { + return new BackupController(formatController, context.getApplicationInfo().dataDir); + } } diff --git a/app/src/main/res/drawable/ic_backup.xml b/app/src/main/res/drawable/ic_backup.xml new file mode 100644 index 0000000..8235e92 --- /dev/null +++ b/app/src/main/res/drawable/ic_backup.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/layout/activity_backup.xml b/app/src/main/res/layout/activity_backup.xml new file mode 100644 index 0000000..66296a8 --- /dev/null +++ b/app/src/main/res/layout/activity_backup.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + +