diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5fb4a5c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/privacy-friendly-backup-api"] + path = libs/privacy-friendly-backup-api + url = https://github.com/SecUSo/privacy-friendly-backup-api.git diff --git a/app/build.gradle b/app/build.gradle index d00c581..24ca532 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,6 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + android { compileSdkVersion 33 @@ -48,4 +50,21 @@ dependencies { testImplementation 'junit:junit:4.12' // https://github.com/ShawnLin013/NumberPicker implementation 'com.shawnlin:number-picker:2.4.4' + + // Backup + implementation project(path: ':backup-api') + def work_version = "2.4.0" + implementation "androidx.work:work-runtime:$work_version" + implementation "androidx.work:work-runtime-ktx:$work_version" + androidTestImplementation "androidx.work:work-testing:$work_version" + implementation 'androidx.sqlite:sqlite-ktx:2.3.1' + + constraints { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") { + because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") + } + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") { + because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") + } + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 897603e..9707c46 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,7 @@ - + @@ -11,6 +13,7 @@ + + + + + @@ -127,6 +140,11 @@ + + \ No newline at end of file diff --git a/app/src/main/java/org/secuso/aktivpause/PFAktivpause.kt b/app/src/main/java/org/secuso/aktivpause/PFAktivpause.kt new file mode 100644 index 0000000..05b7dd9 --- /dev/null +++ b/app/src/main/java/org/secuso/aktivpause/PFAktivpause.kt @@ -0,0 +1,21 @@ +package org.secuso.aktivpause + +import android.app.Application +import android.util.Log +import androidx.work.Configuration +import org.secuso.privacyfriendlybackup.api.pfa.BackupManager +import org.secuso.aktivpause.backup.BackupCreator +import org.secuso.aktivpause.backup.BackupRestorer + +class PFAktivpause : Application(), Configuration.Provider { + + override fun onCreate() { + super.onCreate() + BackupManager.backupCreator = BackupCreator() + BackupManager.backupRestorer = BackupRestorer() + } + + override fun getWorkManagerConfiguration(): Configuration { + return Configuration.Builder().setMinimumLoggingLevel(Log.INFO).build() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/aktivpause/backup/BackupCreator.kt b/app/src/main/java/org/secuso/aktivpause/backup/BackupCreator.kt new file mode 100644 index 0000000..d97a765 --- /dev/null +++ b/app/src/main/java/org/secuso/aktivpause/backup/BackupCreator.kt @@ -0,0 +1,55 @@ +package org.secuso.aktivpause.backup + + +import android.content.Context +import android.preference.PreferenceManager +import android.util.JsonWriter +import android.util.Log +import org.secuso.privacyfriendlybackup.api.backup.DatabaseUtil.getSupportSQLiteOpenHelper +import org.secuso.privacyfriendlybackup.api.backup.DatabaseUtil.writeDatabase +import org.secuso.privacyfriendlybackup.api.backup.PreferenceUtil.writePreferences +import org.secuso.privacyfriendlybackup.api.pfa.IBackupCreator +import org.secuso.aktivpause.database.SQLiteHelper +import java.io.OutputStream +import java.io.OutputStreamWriter + +class BackupCreator : IBackupCreator { + override fun writeBackup(context: Context, outputStream: OutputStream): Boolean { + Log.d(TAG, "createBackup() started") + val outputStreamWriter = OutputStreamWriter(outputStream, Charsets.UTF_8) + val writer = JsonWriter(outputStreamWriter) + writer.setIndent("") + + try { + writer.beginObject() + + Log.d(TAG, "Writing database") + writer.name("database") + + val database = getSupportSQLiteOpenHelper(context, SQLiteHelper.DATABASE_NAME).readableDatabase + + writeDatabase(writer, database) + database.close() + + Log.d(TAG, "Writing preferences") + writer.name("preferences") + + val pref = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) + writePreferences(writer, pref) + + writer.endObject() + writer.close() + } catch (e: Exception) { + Log.e(TAG, "Error occurred", e) + e.printStackTrace() + return false + } + + Log.d(TAG, "Backup created successfully") + return true + } + + companion object { + const val TAG = "PFABackupCreator" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/aktivpause/backup/BackupRestorer.kt b/app/src/main/java/org/secuso/aktivpause/backup/BackupRestorer.kt new file mode 100644 index 0000000..1a950b6 --- /dev/null +++ b/app/src/main/java/org/secuso/aktivpause/backup/BackupRestorer.kt @@ -0,0 +1,142 @@ +package org.secuso.aktivpause.backup + +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import android.util.JsonReader +import android.util.Log +import androidx.annotation.NonNull +import org.secuso.privacyfriendlybackup.api.backup.DatabaseUtil +import org.secuso.privacyfriendlybackup.api.backup.FileUtil +import org.secuso.privacyfriendlybackup.api.pfa.IBackupRestorer +import org.secuso.aktivpause.database.SQLiteHelper +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import kotlin.system.exitProcess + + +class BackupRestorer : IBackupRestorer { + @Throws(IOException::class) + private fun readDatabase(reader: JsonReader, context: Context) { + reader.beginObject() + val n1: String = reader.nextName() + if (n1 != "version") { + throw RuntimeException("Unknown value $n1") + } + val version: Int = reader.nextInt() + val n2: String = reader.nextName() + if (n2 != "content") { + throw RuntimeException("Unknown value $n2") + } + + Log.d(TAG, "Restoring database...") + val restoreDatabaseName = "restoreDatabase" + + // delete if file already exists + val restoreDatabaseFile = context.getDatabasePath(restoreDatabaseName) + if (restoreDatabaseFile.exists()) { + DatabaseUtil.deleteRoomDatabase(context, restoreDatabaseName) + } + + // create new restore database + val db = DatabaseUtil.getSupportSQLiteOpenHelper(context, restoreDatabaseName, version).writableDatabase + + db.beginTransaction() + db.version = version + + Log.d(TAG, "Copying database contents...") + DatabaseUtil.readDatabaseContent(reader, db) + Log.d(TAG, "succesfully read database") + db.setTransactionSuccessful() + db.endTransaction() + db.close() + + reader.endObject() + + // copy file to correct location + val actualDatabaseFile = context.getDatabasePath(SQLiteHelper.DATABASE_NAME) + + DatabaseUtil.deleteRoomDatabase(context, SQLiteHelper.DATABASE_NAME) + + FileUtil.copyFile(restoreDatabaseFile, actualDatabaseFile) + Log.d(TAG, "Database Restored") + + // delete restore database + DatabaseUtil.deleteRoomDatabase(context, restoreDatabaseName) + } + + @Throws(IOException::class) + private fun readPreferences(reader: JsonReader, preferences: SharedPreferences.Editor) { + reader.beginObject() + while (reader.hasNext()) { + val name: String = reader.nextName() + Log.d("preference", name) + when (name) { + "pref_schedule_exercise", + "pref_keep_screen_on_during_exercise", + "REPEAT_STATUS", + "pref_hide_default_exercise_sets", + "pref_schedule_exercise_daystrigger", + "pref_exercise_continuous", + "IsFirstTimeLaunch", + "pref_schedule_random_exercise", + "REPEAT_EXERCISES" -> preferences.putBoolean(name, reader.nextBoolean()) + "pref_exercise_time" -> preferences.putString(name, reader.nextString()) + "FirstLaunchManager.PREF_PICKER_SECONDS", + "FirstLaunchManager.PREF_PICKER_Minutes", + "FirstLaunchManager.PREF_BREAK_PICKER_SECONDS", + "FirstLaunchManager.PREF_PICKER_HOURS", + "FirstLaunchManager.PREF_BREAK_PICKER_MINUTES" -> preferences.putInt(name, reader.nextInt()) + "pref_schedule_exercise_days" -> preferences.putStringSet(name, readPreferenceSet(reader)) + "WORK_TIME", + "PAUSE TIME", + "pref_schedule_exercise_time", + "DEFAULT_EXERCISE_SET" -> preferences.putLong(name, reader.nextLong()) + else -> throw RuntimeException("Unknown preference $name") + } + } + reader.endObject() + } + + private fun readPreferenceSet(reader: JsonReader): Set { + val preferenceSet = mutableSetOf() + + reader.beginArray() + while (reader.hasNext()) { + preferenceSet.add(reader.nextString()); + } + reader.endArray() + return preferenceSet + } + + override fun restoreBackup(context: Context, restoreData: InputStream): Boolean { + return try { + val isReader = InputStreamReader(restoreData) + val reader = JsonReader(isReader) + val preferences = PreferenceManager.getDefaultSharedPreferences(context).edit() + + // START + reader.beginObject() + while (reader.hasNext()) { + val type: String = reader.nextName() + when (type) { + "database" -> readDatabase(reader, context) + "preferences" -> readPreferences(reader, preferences) + else -> throw RuntimeException("Can not parse type $type") + } + } + reader.endObject() + preferences.commit() + + exitProcess(0) + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + companion object { + const val TAG = "PFABackupRestorer" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/aktivpause/backup/PFABackupService.kt b/app/src/main/java/org/secuso/aktivpause/backup/PFABackupService.kt new file mode 100644 index 0000000..7d9a17f --- /dev/null +++ b/app/src/main/java/org/secuso/aktivpause/backup/PFABackupService.kt @@ -0,0 +1,5 @@ +package org.secuso.aktivpause.backup + +import org.secuso.privacyfriendlybackup.api.pfa.PFAAuthService + +class PFABackupService : PFAAuthService() \ No newline at end of file diff --git a/app/src/main/java/org/secuso/aktivpause/database/SQLiteHelper.java b/app/src/main/java/org/secuso/aktivpause/database/SQLiteHelper.java index 2073690..ac61554 100644 --- a/app/src/main/java/org/secuso/aktivpause/database/SQLiteHelper.java +++ b/app/src/main/java/org/secuso/aktivpause/database/SQLiteHelper.java @@ -27,7 +27,7 @@ public class SQLiteHelper extends SQLiteAssetHelper { private static final String TAG = SQLiteHelper.class.getSimpleName(); - private static final String DATABASE_NAME = "exercises.sqlite"; + public static final String DATABASE_NAME = "exercises.sqlite"; private static final int DATABASE_VERSION = 1; private static final String[] deleteQueryList = { diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 41235bd..e2cc9ab 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,4 +1,4 @@ - +?xml version="1.0" encoding="utf-8"?>