Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements Backup Improvements for 3.4 #776

Merged
merged 3 commits into from
Sep 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
android:versionName="3.3.386">

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
Expand Down
6 changes: 0 additions & 6 deletions app/src/main/java/net/bible/android/SharedConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ object SharedConstants {
const val REQUIRED_MEGS_FOR_DOWNLOADS: Long = 50
const val NO_VALUE = -1

private const val BACKUP_SUBDIR_NAME = "andbible_backup"
private const val CSS_SUBDIR_NAME = "css"

private const val MANUAL_INSTALL_SUBDIR = "jsword"
Expand Down Expand Up @@ -62,9 +61,4 @@ object SharedConstants {
return File(sdcard, MANUAL_INSTALL_SUBDIR)
}

val backupDir: File
get() {
val sdcard = Environment.getExternalStorageDirectory()
return File(sdcard, BACKUP_SUBDIR_NAME)
}
}
156 changes: 79 additions & 77 deletions app/src/main/java/net/bible/android/control/backup/BackupControl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.bible.android.activity.BuildConfig
import net.bible.android.database.DATABASE_VERSION
import net.bible.android.view.activity.page.MainBibleActivity
import net.bible.android.view.activity.page.MainBibleActivity.Companion.mainBibleActivity
import net.bible.android.view.util.Hourglass
import net.bible.service.common.CommonUtils
Expand Down Expand Up @@ -73,30 +74,9 @@ import kotlin.coroutines.resume
@ApplicationScope
class BackupControl @Inject constructor() {

/** return true if a backup has been done and the file is on the sd card.
*/
private val isBackupFileExists: Boolean
get() = File(SharedConstants.backupDir, DATABASE_NAME).exists()

/** backup database to sd card
*/
fun backupDatabase() {
mainBibleActivity.windowRepository.saveIntoDb()
db.sync()
val ok = FileManager.copyFile(DATABASE_NAME, internalDbDir, SharedConstants.backupDir)

if (ok) {
Log.d(TAG, "Copied database to internal memory successfully")
Dialogs.instance.showMsg(R.string.backup_success, SharedConstants.backupDir.absolutePath)
} else {
Log.e(TAG, "Error copying database to internal memory")
Dialogs.instance.showErrorMsg(R.string.error_occurred)
}
}

/** Backup database to Uri returned from ACTION_CREATE_DOCUMENT intent
*/
fun backupDatabaseToUri( uri: Uri) : Boolean {
suspend fun backupDatabaseToUri( uri: Uri) {
val out = BibleApplication.application.contentResolver.openOutputStream(uri)!!
val filename = DATABASE_NAME;
val f = File(internalDbDir, filename);
Expand All @@ -111,15 +91,15 @@ class BackupControl @Inject constructor() {
ok = false
}

if (ok) {
Log.d(TAG, "Copied database to chosen backup location successfully")
Dialogs.instance.showMsg(R.string.backup_success, uri.path)
mattschlosser marked this conversation as resolved.
Show resolved Hide resolved
} else {
Log.e(TAG, "Error copying database to chosen location.")
Dialogs.instance.showErrorMsg(R.string.error_occurred)
withContext(Dispatchers.Main) {
if (ok) {
Log.d(TAG, "Copied database to chosen backup location successfully")
Dialogs.instance.showMsg(R.string.backup_success)
} else {
Log.e(TAG, "Error copying database to chosen location.")
Dialogs.instance.showErrorMsg(R.string.error_occurred)
}
}

return ok;
}

/** backup database to custom target (email, drive etc.) via ACTION_SEND intent
Expand Down Expand Up @@ -150,9 +130,9 @@ class BackupControl @Inject constructor() {
f.delete()
}

/** backup database from custom source
/** restore database from custom source
*/
fun restoreDatabaseViaIntent(inputStream: InputStream): Boolean {
suspend fun restoreDatabaseViaIntent(inputStream: InputStream): Boolean {
val fileName = DATABASE_NAME
internalDbBackupDir.mkdirs()
val f = File(internalDbBackupDir, fileName)
Expand All @@ -173,14 +153,15 @@ class BackupControl @Inject constructor() {
}
sqlDb.close()
}

if (ok) {
ABEventBus.getDefault().post(SynchronizeWindowsEvent(true))
Log.d(TAG, "Restored database successfully")
Dialogs.instance.showMsg(R.string.restore_success)
} else {
Log.e(TAG, "Error restoring database")
Dialogs.instance.showErrorMsg(R.string.restore_unsuccessfull)
withContext(Dispatchers.Main) {
if (ok) {
ABEventBus.getDefault().post(SynchronizeWindowsEvent(true))
Log.d(TAG, "Restored database successfully")
Dialogs.instance.showMsg(R.string.restore_success)
} else {
Log.e(TAG, "Error restoring database")
Dialogs.instance.showErrorMsg(R.string.restore_unsuccessfull)
}
}
f.delete()
return ok
Expand Down Expand Up @@ -279,8 +260,32 @@ class BackupControl @Inject constructor() {
internalDbBackupDir.deleteRecursively()
}

suspend fun backupModulesViaIntent(callingActivity: Activity) = withContext(Dispatchers.Main) {
val fileName = "modules.zip"
suspend fun backupModulesToUri(uri: Uri) {
// at this point the zip file has already been created
val fileName = MODULE_BACKUP_NAME
val zipFile = File(internalDbBackupDir, fileName)
val out = BibleApplication.application.contentResolver.openOutputStream(uri)!!
val inputStream = FileInputStream(zipFile)
var ok = true
try {
out.write(inputStream.readBytes())
out.close()
} catch (ex: IOException) {
ok = false
}
withContext(Dispatchers.Main) {
if (ok) {
Log.d(TAG, "Copied modules to chosen backup location successfully")
Dialogs.instance.showMsg(R.string.backup_modules_success)
} else {
Log.e(TAG, "Error copying modules to chosen location.")
Dialogs.instance.showErrorMsg(R.string.error_occurred)
}
}
}

suspend fun backupModulesViaIntent(callingActivity: Activity) = withContext(Dispatchers.Main) {
val fileName = MODULE_BACKUP_NAME
internalDbBackupDir.mkdirs()
val zipFile = File(internalDbBackupDir, fileName)
val books = selectModules(callingActivity) ?: return@withContext
Expand All @@ -290,51 +295,48 @@ class BackupControl @Inject constructor() {
createZip(books, zipFile)
hourglass.dismiss()

val modulesString = books.joinToString(", ") { it.abbreviation }
val subject = BibleApplication.application.getString(R.string.backup_modules_email_subject_2, CommonUtils.applicationNameMedium)
val message = BibleApplication.application.getString(R.string.backup_modules_email_message_2, CommonUtils.applicationNameMedium, modulesString)
// send intent to pick file

val uri = FileProvider.getUriForFile(callingActivity, BuildConfig.APPLICATION_ID + ".provider", zipFile)
val email = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, message)
type = "application/zip"
}
val chooserIntent = Intent.createChooser(email, getString(R.string.send_backup_file))
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
callingActivity.startActivity(chooserIntent)
}

/** restore database from sd card
*/
fun restoreDatabase() {
if (!isBackupFileExists) {
Dialogs.instance.showErrorMsg(R.string.error_no_backup_file)
} else {
Dialogs.instance.showMsg(R.string.restore_confirmation, true) {
BibleApplication.application.deleteDatabase(DATABASE_NAME)
val ok = FileManager.copyFile(DATABASE_NAME, SharedConstants.backupDir, internalDbDir)

if (ok) {
DatabaseContainer.reset()
ABEventBus.getDefault().post(SynchronizeWindowsEvent(true))
Log.d(TAG, "Copied database from internal memory successfully")
Dialogs.instance.showMsg(R.string.restore_success, SharedConstants.backupDir.name)
} else {
Log.e(TAG, "Error copying database from internal memory")
Dialogs.instance.showErrorMsg(R.string.error_occurred)
AlertDialog.Builder(callingActivity)
.setTitle(callingActivity.getString(R.string.backup_backup_title))
.setMessage(callingActivity.getString(R.string.backup_backup_message))
.setNegativeButton(callingActivity.getString(R.string.backup_phone_storage)) { dialog, which ->
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/zip"
putExtra(Intent.EXTRA_TITLE, fileName)
}
callingActivity.startActivityForResult(intent, MainBibleActivity.REQUEST_PICK_FILE_FOR_BACKUP_MODULES)
}
}
.setPositiveButton(callingActivity.getString(R.string.backup_share)) { dialog, which ->
val modulesString = books.joinToString(", ") { it.abbreviation }
val subject = BibleApplication.application.getString(R.string.backup_modules_email_subject_2, CommonUtils.applicationNameMedium)
val message = BibleApplication.application.getString(R.string.backup_modules_email_message_2, CommonUtils.applicationNameMedium, modulesString)

val uri = FileProvider.getUriForFile(callingActivity, BuildConfig.APPLICATION_ID + ".provider", zipFile)
val email = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, message)
type = "application/zip"
}
val chooserIntent = Intent.createChooser(email, getString(R.string.send_backup_file))
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
callingActivity.startActivity(chooserIntent)
}

.setNeutralButton(callingActivity.getString(R.string.cancel), null)
.show()

}


companion object {

// this is now unused because And Bible databases are held on the SD card to facilitate easier backup by file copy
private lateinit var internalDbDir : File;
private lateinit var internalDbBackupDir: File;

private const val MODULE_BACKUP_NAME = "modules.zip"
fun setupDirs(context: Context) {
internalDbDir = File(context.getDatabasePath(DATABASE_NAME).parent!!)
internalDbBackupDir = File(context.filesDir, "/backup")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,11 @@ class MainBibleActivity : CustomTitlebarActivityBase(), VerseActionModeMediator.
backupControl.backupDatabaseToUri(data!!.data!!)
}
}
REQUEST_PICK_FILE_FOR_BACKUP_MODULES -> {
GlobalScope.launch(Dispatchers.IO) {
backupControl.backupModulesToUri(data!!.data!!)
}
}
WORKSPACE_CHANGED -> {
val extras = data?.extras
val workspaceId = extras?.getLong("workspaceId")
Expand Down Expand Up @@ -1295,16 +1300,6 @@ class MainBibleActivity : CustomTitlebarActivityBase(), VerseActionModeMediator.
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
when (requestCode) {
BACKUP_SAVE_REQUEST -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
backupControl.backupDatabase()
} else {
Dialogs.instance.showMsg(R.string.error_occurred)
}
BACKUP_RESTORE_REQUEST -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
backupControl.restoreDatabase()
} else {
Dialogs.instance.showMsg(R.string.error_occurred)
}
SDCARD_READ_REQUEST -> if (grantResults.isNotEmpty()) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
documentControl.enableManualInstallFolder()
Expand Down Expand Up @@ -1431,8 +1426,6 @@ class MainBibleActivity : CustomTitlebarActivityBase(), VerseActionModeMediator.

companion object {
lateinit var mainBibleActivity: MainBibleActivity
internal const val BACKUP_SAVE_REQUEST = 0
internal const val BACKUP_RESTORE_REQUEST = 1
var initialized = false
private const val SDCARD_READ_REQUEST = 2

Expand All @@ -1441,6 +1434,7 @@ class MainBibleActivity : CustomTitlebarActivityBase(), VerseActionModeMediator.
const val COLORS_CHANGED = 5
const val WORKSPACE_CHANGED = 6
const val REQUEST_PICK_FILE_FOR_BACKUP_DB = 7
const val REQUEST_PICK_FILE_FOR_BACKUP_MODULES = 8


private const val SCREEN_KEEP_ON_PREF = "screen_keep_on_pref"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ import net.bible.android.view.activity.installzip.InstallZip
import net.bible.android.view.activity.mynote.MyNotes
import net.bible.android.view.activity.navigation.ChooseDocument
import net.bible.android.view.activity.navigation.History
import net.bible.android.view.activity.page.MainBibleActivity.Companion.BACKUP_RESTORE_REQUEST
import net.bible.android.view.activity.page.MainBibleActivity.Companion.BACKUP_SAVE_REQUEST
import net.bible.android.view.activity.page.MainBibleActivity.Companion.REQUEST_PICK_FILE_FOR_BACKUP_DB
import net.bible.android.view.activity.page.MainBibleActivity.Companion.REQUEST_PICK_FILE_FOR_BACKUP_RESTORE
import net.bible.android.view.activity.readingplan.DailyReading
Expand Down Expand Up @@ -220,20 +218,12 @@ constructor(private val callingActivity: MainBibleActivity,
.setTitle(callingActivity.getString(R.string.backup_backup_title))
.setMessage(callingActivity.getString(R.string.backup_backup_message))
.setNegativeButton(callingActivity.getString(R.string.backup_phone_storage)) { dialog, which ->
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is removed, I think this needs testing on those android versions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would indeed be good to roughly go through different android versions with emulator (from lollipop (5) to the latest models).

if (ContextCompat.checkSelfPermission(callingActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(callingActivity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), BACKUP_SAVE_REQUEST)
} else {
backupControl.backupDatabase()
}
} else {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-sqlite3"
putExtra(Intent.EXTRA_TITLE, DATABASE_NAME)
}
callingActivity.startActivityForResult(intent, REQUEST_PICK_FILE_FOR_BACKUP_DB)
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-sqlite3"
putExtra(Intent.EXTRA_TITLE, DATABASE_NAME)
}
callingActivity.startActivityForResult(intent, REQUEST_PICK_FILE_FOR_BACKUP_DB)
}
.setPositiveButton(callingActivity.getString(R.string.backup_share)) { dialog, which ->
backupControl.backupDatabaseViaSendIntent(callingActivity)
Expand All @@ -259,29 +249,9 @@ constructor(private val callingActivity: MainBibleActivity,
requestCode = IntentHelper.UPDATE_SUGGESTED_DOCUMENTS_ON_FINISH
}
R.id.restore_app_database -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is removed, I think this needs testing on those android versions.

AlertDialog.Builder(callingActivity)
.setTitle(callingActivity.getString(R.string.backup_restore_title))
.setMessage(callingActivity.getString(R.string.backup_restore_message))
.setNegativeButton(callingActivity.getString(R.string.backup_phone_storage)) { dialog, which ->
if (ContextCompat.checkSelfPermission(callingActivity, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(callingActivity, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), BACKUP_RESTORE_REQUEST)
} else {
backupControl.restoreDatabase()
}
}
.setPositiveButton(callingActivity.getString(R.string.backup_manually)) { dialog, which ->
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/*"
callingActivity.startActivityForResult(intent, REQUEST_PICK_FILE_FOR_BACKUP_RESTORE)
}
.setNeutralButton(callingActivity.getString(R.string.cancel), null)
.show()
} else {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/*"
callingActivity.startActivityForResult(intent, REQUEST_PICK_FILE_FOR_BACKUP_RESTORE)
}
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/*"
callingActivity.startActivityForResult(intent, REQUEST_PICK_FILE_FOR_BACKUP_RESTORE)
isHandled = true
}
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,8 @@
<string name="restore_confirmation">Overwrite My Notes, Bookmarks, Reading Plans and Workspaces?</string>
<string name="restore_success">My Notes, Bookmarks, Reading Plans and Workspaces restored successfully</string>
<string name="loading_backup">Loading database from backup…</string>
<string name="backup_success">My Notes, Bookmarks, Reading Plans and Workspaces backed up successfully to %s.</string>
<string name="backup_success">My Notes, Bookmarks, Reading Plans and Workspaces backed up successfully.</string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of translations, we should not make such changes to strings that change parameters. Otherwise users with translations that have not been updated will have crashes.

When parameters need to be changed, like here, we need to introduce a new string (with new key, for example backup_success2), use it, and remove old string.

<string name="backup_modules_success">Selected modules backed up successfully.</string>
<string name="error_no_backup_file">Backup file was not found</string>

<string name="backup_modules_email_subject_2">%s modules backup</string>
Expand Down