From d86d12f2db3130cff08ec31a63e9c36deacd4b1a Mon Sep 17 00:00:00 2001 From: Aditya Wasan Date: Fri, 2 Apr 2021 13:25:43 +0530 Subject: [PATCH 1/6] database: convert to multiplatform Co-authored-by: Harsh Shandilya Signed-off-by: Aditya Wasan Signed-off-by: Harsh Shandilya --- .idea/artifacts/database_jvm.xml | 8 ++++ app/build.gradle.kts | 1 - .../lobsters/injection/DatabaseModule.kt | 24 +++------- database/build.gradle.kts | 47 +++++++++++++++++-- database/src/androidMain/AndroidManifest.xml | 2 + .../msfjarvis/lobsters/data/local/Database.kt | 12 +++++ .../msfjarvis/lobsters/data/local/Database.kt | 18 +++++++ .../lobsters/data}/model/TagsAdapter.kt | 2 +- .../lobsters/data/local/SavedPost.sq | 0 .../msfjarvis/lobsters/data/local/Database.kt | 13 +++++ .../data/local/SqlDelightQueriesTest.kt | 2 +- 11 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 .idea/artifacts/database_jvm.xml create mode 100644 database/src/androidMain/AndroidManifest.xml create mode 100644 database/src/androidMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt create mode 100644 database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt rename database/src/{main/java/dev/msfjarvis/lobsters => commonMain/kotlin/dev/msfjarvis/lobsters/data}/model/TagsAdapter.kt (90%) rename database/src/{main => commonMain}/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq (100%) create mode 100644 database/src/jvmMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt rename database/src/{test/java => jvmTest/kotlin}/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt (98%) diff --git a/.idea/artifacts/database_jvm.xml b/.idea/artifacts/database_jvm.xml new file mode 100644 index 00000000..a32d631c --- /dev/null +++ b/.idea/artifacts/database_jvm.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/database/build/libs + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 39eec84b..0d31a976 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { implementation(Dependencies.ThirdParty.accompanistFlow) implementation(Dependencies.ThirdParty.Moshi.lib) implementation(Dependencies.ThirdParty.Retrofit.moshi) - implementation(Dependencies.ThirdParty.SQLDelight.androidDriver) testImplementation(kotlin("test-junit")) androidTestImplementation(kotlin("test-junit")) } diff --git a/app/src/main/java/dev/msfjarvis/lobsters/injection/DatabaseModule.kt b/app/src/main/java/dev/msfjarvis/lobsters/injection/DatabaseModule.kt index a960454d..6638523a 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/injection/DatabaseModule.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/injection/DatabaseModule.kt @@ -1,41 +1,29 @@ package dev.msfjarvis.lobsters.injection import android.content.Context -import com.squareup.sqldelight.android.AndroidSqliteDriver -import com.squareup.sqldelight.db.SqlDriver import dagger.Module import dagger.Provides -import dagger.Reusable import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import dev.msfjarvis.lobsters.data.local.SavedPost +import dev.msfjarvis.lobsters.data.local.DriverFactory +import dev.msfjarvis.lobsters.data.local.createDatabase import dev.msfjarvis.lobsters.database.LobstersDatabase -import dev.msfjarvis.lobsters.model.TagsAdapter import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DatabaseModule { - @Provides - @Reusable - fun providesTagsAdapter(): TagsAdapter { - return TagsAdapter() - } - @Provides @Singleton - fun providesSqlDriver(@ApplicationContext context: Context): SqlDriver { - return AndroidSqliteDriver(LobstersDatabase.Schema, context, "SavedPosts.db") + fun providesDriverFactory(@ApplicationContext context: Context): DriverFactory { + return DriverFactory(context) } @Provides @Singleton - fun providesLobstersDatabase(sqlDriver: SqlDriver, tagsAdapter: TagsAdapter): LobstersDatabase { - return LobstersDatabase( - sqlDriver, - SavedPost.Adapter(tagsAdapter), - ) + fun providesLobstersDatabase(driverFactory: DriverFactory): LobstersDatabase { + return createDatabase(driverFactory) } } diff --git a/database/build.gradle.kts b/database/build.gradle.kts index daaf6ca9..763013a7 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -1,11 +1,48 @@ plugins { - kotlin("jvm") + kotlin("multiplatform") + id("com.android.library") id("com.squareup.sqldelight") `lobsters-plugin` } -dependencies { - testImplementation(Dependencies.Kotlin.Coroutines.core) - testImplementation(Dependencies.ThirdParty.SQLDelight.jvmDriver) - testImplementation(kotlin("test-junit")) +// workaround for https://youtrack.jetbrains.com/issue/KT-43944 +android { + configurations { + create("androidTestApi") + create("androidTestDebugApi") + create("androidTestReleaseApi") + create("testApi") + create("testDebugApi") + create("testReleaseApi") + } +} + +kotlin { + jvm() + android() + + sourceSets { + val commonMain by getting { + } + val jvmTest by getting { + dependencies { + implementation(Dependencies.Kotlin.Coroutines.core) + implementation(kotlin("test-junit")) + } + } + val jvmMain by getting { + dependencies { + implementation(Dependencies.ThirdParty.SQLDelight.jvmDriver) + } + } + val androidMain by getting { + dependencies { + implementation(Dependencies.ThirdParty.SQLDelight.androidDriver) + } + } + } +} + +android { + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") } diff --git a/database/src/androidMain/AndroidManifest.xml b/database/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..4ee754c1 --- /dev/null +++ b/database/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/database/src/androidMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt b/database/src/androidMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt new file mode 100644 index 00000000..37209b72 --- /dev/null +++ b/database/src/androidMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt @@ -0,0 +1,12 @@ +package dev.msfjarvis.lobsters.data.local + +import android.content.Context +import com.squareup.sqldelight.android.AndroidSqliteDriver +import com.squareup.sqldelight.db.SqlDriver +import dev.msfjarvis.lobsters.database.LobstersDatabase + +actual class DriverFactory(private val context: Context) { + actual fun createDriver(): SqlDriver { + return AndroidSqliteDriver(LobstersDatabase.Schema, context, LobstersDatabaseName) + } +} diff --git a/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt b/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt new file mode 100644 index 00000000..1ab3266e --- /dev/null +++ b/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt @@ -0,0 +1,18 @@ +package dev.msfjarvis.lobsters.data.local + +import com.squareup.sqldelight.db.SqlDriver +import dev.msfjarvis.lobsters.data.model.TagsAdapter +import dev.msfjarvis.lobsters.database.LobstersDatabase + +internal const val LobstersDatabaseName = "SavedPosts.db" + +expect class DriverFactory { + fun createDriver(): SqlDriver +} + +private fun getTagsAdapter() = TagsAdapter() + +fun createDatabase(driverFactory: DriverFactory): LobstersDatabase { + val driver = driverFactory.createDriver() + return LobstersDatabase(driver, SavedPost.Adapter(getTagsAdapter())) +} diff --git a/database/src/main/java/dev/msfjarvis/lobsters/model/TagsAdapter.kt b/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/model/TagsAdapter.kt similarity index 90% rename from database/src/main/java/dev/msfjarvis/lobsters/model/TagsAdapter.kt rename to database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/model/TagsAdapter.kt index a4a04588..3b8fcb48 100644 --- a/database/src/main/java/dev/msfjarvis/lobsters/model/TagsAdapter.kt +++ b/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/model/TagsAdapter.kt @@ -1,4 +1,4 @@ -package dev.msfjarvis.lobsters.model +package dev.msfjarvis.lobsters.data.model import com.squareup.sqldelight.ColumnAdapter diff --git a/database/src/main/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq b/database/src/commonMain/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq similarity index 100% rename from database/src/main/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq rename to database/src/commonMain/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq diff --git a/database/src/jvmMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt b/database/src/jvmMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt new file mode 100644 index 00000000..9883a8a3 --- /dev/null +++ b/database/src/jvmMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt @@ -0,0 +1,13 @@ +package dev.msfjarvis.lobsters.data.local + +import com.squareup.sqldelight.db.SqlDriver +import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver +import dev.msfjarvis.lobsters.database.LobstersDatabase + +actual class DriverFactory { + actual fun createDriver(): SqlDriver { + val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + LobstersDatabase.Schema.create(driver) + return driver + } +} diff --git a/database/src/test/java/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt b/database/src/jvmTest/kotlin/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt similarity index 98% rename from database/src/test/java/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt rename to database/src/jvmTest/kotlin/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt index 68bee231..5ca12981 100644 --- a/database/src/test/java/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt +++ b/database/src/jvmTest/kotlin/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt @@ -1,8 +1,8 @@ package dev.msfjarvis.lobsters.data.local import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver +import dev.msfjarvis.lobsters.data.model.TagsAdapter import dev.msfjarvis.lobsters.database.LobstersDatabase -import dev.msfjarvis.lobsters.model.TagsAdapter import kotlin.test.Test import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking From a08109d7455b9bc28c0c361a43c897a64d0ba23a Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 2 Apr 2021 13:32:19 +0530 Subject: [PATCH 2/6] app: add BackupHandler Signed-off-by: Harsh Shandilya --- .../lobsters/data/backup/BackupHandler.kt | 32 +++++++++++++++++++ .../lobsters/injection/BackupModule.kt | 21 ++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 app/src/main/java/dev/msfjarvis/lobsters/data/backup/BackupHandler.kt create mode 100644 app/src/main/java/dev/msfjarvis/lobsters/injection/BackupModule.kt diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/backup/BackupHandler.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/backup/BackupHandler.kt new file mode 100644 index 00000000..6af9b3ac --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/backup/BackupHandler.kt @@ -0,0 +1,32 @@ +package dev.msfjarvis.lobsters.data.backup + +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import dev.msfjarvis.lobsters.data.local.SavedPost +import dev.msfjarvis.lobsters.database.LobstersDatabase +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalStdlibApi::class) +class BackupHandler +@Inject +constructor( + private val database: LobstersDatabase, + moshi: Moshi, +) { + private val adapter = moshi.adapter>() + + suspend fun exportSavedPosts(): String { + val posts = + withContext(Dispatchers.IO) { database.savedPostQueries.selectAllPosts().executeAsList() } + return adapter.toJson(posts) + } + + suspend fun importSavedPosts(json: String) { + withContext(Dispatchers.IO) { + val posts = requireNotNull(adapter.fromJson(json)) + database.transaction { posts.forEach { database.savedPostQueries.insertOrReplacePost(it) } } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/lobsters/injection/BackupModule.kt b/app/src/main/java/dev/msfjarvis/lobsters/injection/BackupModule.kt new file mode 100644 index 00000000..545dcc5f --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/lobsters/injection/BackupModule.kt @@ -0,0 +1,21 @@ +package dev.msfjarvis.lobsters.injection + +import com.squareup.moshi.Moshi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dev.msfjarvis.lobsters.data.backup.BackupHandler +import dev.msfjarvis.lobsters.database.LobstersDatabase + +@Module +@InstallIn(ActivityComponent::class) +object BackupModule { + @Provides + fun provideBackupHandler( + database: LobstersDatabase, + moshi: Moshi, + ): BackupHandler { + return BackupHandler(database, moshi) + } +} From ef09b241ea34aaa72015cc9f1c119a71f9cddd15 Mon Sep 17 00:00:00 2001 From: Aditya Wasan Date: Fri, 2 Apr 2021 13:36:41 +0530 Subject: [PATCH 3/6] common/app: add Settings UI composables Co-authored-by: Harsh Shandilya Signed-off-by: Harsh Shandilya --- .../msfjarvis/lobsters/ui/settings/Options.kt | 58 +++++++++++ .../lobsters/ui/settings/Settings.kt | 95 +++++++++++++++++++ .../msfjarvis/lobsters/utils/StringValue.kt | 7 +- common/src/androidMain/res/values/strings.xml | 5 + .../dev/msfjarvis/lobsters/utils/Strings.kt | 5 + .../msfjarvis/lobsters/utils/StringValue.kt | 7 ++ 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt create mode 100644 app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Settings.kt diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt new file mode 100644 index 00000000..e9badec4 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt @@ -0,0 +1,58 @@ +package dev.msfjarvis.lobsters.ui.settings + +import android.content.Context +import androidx.activity.compose.registerForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import dev.msfjarvis.lobsters.data.backup.BackupHandler +import dev.msfjarvis.lobsters.utils.Strings +import dev.msfjarvis.lobsters.utils.get +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +private const val JSON_MINE = "application/json" + +@Composable +fun BackupOption( + context: Context, + backupHandler: BackupHandler, + coroutineScope: CoroutineScope, +) { + val result = + registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri -> + if (uri == null) return@registerForActivityResult + context.contentResolver.openOutputStream(uri)?.use { + coroutineScope.launch(Dispatchers.IO) { + it.write(backupHandler.exportSavedPosts().toByteArray(Charsets.UTF_8)) + } + } + } + SettingsActionItem( + Strings.SettingsBackup.get(), + Strings.SettingsBackupDescription.get(), + onClick = { result.launch("Claw-export.json") } + ) +} + +@Composable +fun RestoreOption( + context: Context, + backupHandler: BackupHandler, + coroutineScope: CoroutineScope, +) { + val result = + registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri == null) return@registerForActivityResult + context.contentResolver.openInputStream(uri)?.use { + coroutineScope.launch(Dispatchers.IO) { + backupHandler.importSavedPosts(it.readBytes().toString(Charsets.UTF_8)) + } + } + } + SettingsActionItem( + title = Strings.SettingsRestore.get(), + description = Strings.SettingsRestoreDescription.get(), + onClick = { result.launch(JSON_MINE) } + ) +} diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Settings.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Settings.kt new file mode 100644 index 00000000..9a43d2c2 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Settings.kt @@ -0,0 +1,95 @@ +package dev.msfjarvis.lobsters.ui.settings + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Icon +import androidx.compose.material.ListItem +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import dev.msfjarvis.lobsters.data.backup.BackupHandler +import dev.msfjarvis.lobsters.utils.Strings +import dev.msfjarvis.lobsters.utils.get +import kotlinx.coroutines.CoroutineScope + +@Composable +fun LobstersSettings( + backupHandler: BackupHandler, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + Scaffold( + topBar = { SettingsTopBar(context) }, + content = { SettingsBody(context, backupHandler, scope) }, + ) +} + +@Composable +fun SettingsTopBar( + context: Context, +) { + TopAppBar( + title = { Text(Strings.Settings.get()) }, + navigationIcon = { + Icon( + Icons.Default.ArrowBack, + contentDescription = Strings.Settings.get(), + modifier = + Modifier.padding(start = 16.dp).clickable { (context as ComponentActivity).finish() }, + ) + }, + ) +} + +@Composable +fun SettingsBody( + context: Context, + backupHandler: BackupHandler, + scope: CoroutineScope, +) { + LazyColumn { + item { + BackupOption( + context, + backupHandler, + scope, + ) + } + item { + RestoreOption( + context, + backupHandler, + scope, + ) + } + } +} + +@Composable +fun SettingsActionItem( + title: String, + description: String? = null, + singleLineDescription: Boolean = true, + icon: ImageVector? = null, + onClick: (() -> Unit)? = null, +) { + ListItem( + text = { Text(title) }, + secondaryText = { description?.let { Text(it) } }, + icon = { icon?.let { Icon(icon, null, Modifier.height(32.dp)) } }, + singleLineSecondaryText = singleLineDescription, + modifier = Modifier.clickable { onClick?.invoke() }, + ) +} diff --git a/common/src/androidMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt b/common/src/androidMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt index 8af20951..c561b196 100644 --- a/common/src/androidMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt +++ b/common/src/androidMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt @@ -11,13 +11,18 @@ private fun stringEnumMapper(stringEnum: Strings): Int { Strings.AvatarContentDescription -> R.string.avatar_content_description Strings.ChangeSortingOrder -> R.string.change_sorting_order Strings.HottestPosts -> R.string.hottest_posts + Strings.NewestPosts -> R.string.newest_posts Strings.NoSavedPost -> R.string.no_saved_posts Strings.OpenComments -> R.string.open_comments Strings.RefreshPostsContentDescription -> R.string.refresh_posts_content_description Strings.RemoveFromSavedPosts -> R.string.remove_from_saved_posts Strings.SavedPosts -> R.string.saved_posts Strings.SubmittedBy -> R.string.submitted_by - Strings.NewestPosts -> R.string.newest_posts + Strings.Settings -> R.string.settings + Strings.SettingsBackup -> R.string.settings_backup + Strings.SettingsBackupDescription -> R.string.settings_backup_desc + Strings.SettingsRestore -> R.string.settings_restore + Strings.SettingsRestoreDescription -> R.string.settings_restore_desc } } diff --git a/common/src/androidMain/res/values/strings.xml b/common/src/androidMain/res/values/strings.xml index 0de8ea60..74e570c2 100644 --- a/common/src/androidMain/res/values/strings.xml +++ b/common/src/androidMain/res/values/strings.xml @@ -11,4 +11,9 @@ Open comments Change sort order Newest + Settings + Backup saved posts + Export saved posts in a JSON file that can be restored later + Restore saved posts + Import a previously exported copy of saved posts. Existing saved posts are not cleared diff --git a/common/src/commonMain/kotlin/dev/msfjarvis/lobsters/utils/Strings.kt b/common/src/commonMain/kotlin/dev/msfjarvis/lobsters/utils/Strings.kt index 13d946a9..cff1c835 100644 --- a/common/src/commonMain/kotlin/dev/msfjarvis/lobsters/utils/Strings.kt +++ b/common/src/commonMain/kotlin/dev/msfjarvis/lobsters/utils/Strings.kt @@ -13,4 +13,9 @@ enum class Strings { SavedPosts, SubmittedBy, NewestPosts, + Settings, + SettingsBackup, + SettingsBackupDescription, + SettingsRestore, + SettingsRestoreDescription, } diff --git a/common/src/jvmMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt b/common/src/jvmMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt index 27f54788..c2a10b61 100644 --- a/common/src/jvmMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt +++ b/common/src/jvmMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt @@ -16,6 +16,13 @@ private fun stringEnumMapper(stringEnum: Strings): String { Strings.SavedPosts -> "Saved" Strings.SubmittedBy -> "submitted by %1s" Strings.NewestPosts -> "Newest" + Strings.Settings -> "Settings" + Strings.SettingsBackup -> "Backup saved posts" + Strings.SettingsBackupDescription -> + "Export saved posts in a JSON file that can be restored later" + Strings.SettingsRestore -> "Restore saved posts" + Strings.SettingsRestoreDescription -> + "Import a previously exported copy of saved posts. Existing saved posts are not cleared" } } From cf5410622f3a875437597b19ecc9858d16df0c9d Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 2 Apr 2021 13:37:26 +0530 Subject: [PATCH 4/6] app: integrate settings into main UI Signed-off-by: Harsh Shandilya --- .../lobsters/ui/main/LobstersTopBarTest.kt | 4 ++++ app/src/main/AndroidManifest.xml | 8 ++++++++ .../msfjarvis/lobsters/ui/main/LobstersApp.kt | 6 +++++- .../lobsters/ui/main/LobstersTopAppBar.kt | 9 +++++++++ .../lobsters/ui/settings/SettingsActivity.kt | 20 +++++++++++++++++++ .../res/drawable/ic_app_settings_24px.xml | 9 +++++++++ 6 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/dev/msfjarvis/lobsters/ui/settings/SettingsActivity.kt create mode 100644 app/src/main/res/drawable/ic_app_settings_24px.xml diff --git a/app/src/androidTest/java/dev/msfjarvis/lobsters/ui/main/LobstersTopBarTest.kt b/app/src/androidTest/java/dev/msfjarvis/lobsters/ui/main/LobstersTopBarTest.kt index cd66ee9b..56aaa61d 100644 --- a/app/src/androidTest/java/dev/msfjarvis/lobsters/ui/main/LobstersTopBarTest.kt +++ b/app/src/androidTest/java/dev/msfjarvis/lobsters/ui/main/LobstersTopBarTest.kt @@ -24,6 +24,7 @@ class LobstersTopBarTest : ScreenshotTest { LobstersTopAppBar( currentDestination = Destination.Hottest, toggleSortingOrder = {}, + launchSettings = {}, ) } } @@ -38,6 +39,7 @@ class LobstersTopBarTest : ScreenshotTest { LobstersTopAppBar( currentDestination = Destination.Hottest, toggleSortingOrder = {}, + launchSettings = {}, ) } } @@ -52,6 +54,7 @@ class LobstersTopBarTest : ScreenshotTest { LobstersTopAppBar( currentDestination = Destination.Saved, toggleSortingOrder = {}, + launchSettings = {}, ) } } @@ -66,6 +69,7 @@ class LobstersTopBarTest : ScreenshotTest { LobstersTopAppBar( currentDestination = Destination.Saved, toggleSortingOrder = {}, + launchSettings = {}, ) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d897211e..8f0e6c9e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,14 @@ + + + + + diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/main/LobstersApp.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/main/LobstersApp.kt index f9a9e9cf..88f438a3 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/main/LobstersApp.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/main/LobstersApp.kt @@ -1,5 +1,6 @@ package dev.msfjarvis.lobsters.ui.main +import android.content.Intent import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.BottomNavigation @@ -11,6 +12,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.KEY_ROUTE @@ -24,6 +26,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import dev.msfjarvis.lobsters.ui.navigation.Destination import dev.msfjarvis.lobsters.ui.posts.NetworkPosts import dev.msfjarvis.lobsters.ui.posts.SavedPosts +import dev.msfjarvis.lobsters.ui.settings.SettingsActivity import dev.msfjarvis.lobsters.ui.viewmodel.LobstersViewModel import dev.msfjarvis.lobsters.util.IconResource import dev.msfjarvis.lobsters.utils.get @@ -32,6 +35,7 @@ import kotlinx.coroutines.launch @Composable fun LobstersApp() { val viewModel: LobstersViewModel = viewModel() + val context = LocalContext.current val navController = rememberNavController() val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems() val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems() @@ -70,7 +74,7 @@ fun LobstersApp() { LobstersTopAppBar( currentDestination, viewModel::toggleSortOrder, - ) + ) { context.startActivity(Intent(context, SettingsActivity::class.java)) } }, bottomBar = { LobstersBottomNav( diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/main/LobstersTopAppBar.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/main/LobstersTopAppBar.kt index fc63e087..3e2a4f51 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/main/LobstersTopAppBar.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/main/LobstersTopAppBar.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.launch fun LobstersTopAppBar( currentDestination: Destination, toggleSortingOrder: suspend () -> Unit, + launchSettings: () -> Unit, ) { val scope = rememberCoroutineScope() TopAppBar( @@ -42,6 +43,14 @@ fun LobstersTopAppBar( }, ) } + IconResource( + resourceId = R.drawable.ic_app_settings_24px, + contentDescription = Strings.Settings.get(), + modifier = + Modifier.padding(horizontal = 8.dp, vertical = 8.dp).clickable { + scope.launch { launchSettings() } + }, + ) } ) } diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/SettingsActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/SettingsActivity.kt new file mode 100644 index 00000000..38961b4e --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/SettingsActivity.kt @@ -0,0 +1,20 @@ +package dev.msfjarvis.lobsters.ui.settings + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import dagger.hilt.android.AndroidEntryPoint +import dev.msfjarvis.lobsters.data.backup.BackupHandler +import dev.msfjarvis.lobsters.ui.theme.LobstersTheme +import javax.inject.Inject + +@AndroidEntryPoint +class SettingsActivity : ComponentActivity() { + + @Inject lateinit var backupHandler: BackupHandler + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { LobstersTheme { LobstersSettings(backupHandler) } } + } +} diff --git a/app/src/main/res/drawable/ic_app_settings_24px.xml b/app/src/main/res/drawable/ic_app_settings_24px.xml new file mode 100644 index 00000000..0c0c36b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_app_settings_24px.xml @@ -0,0 +1,9 @@ + + + From 31fbc8c95c60bf10baeb5b252eb8aaed708065f1 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sat, 3 Apr 2021 15:08:09 +0530 Subject: [PATCH 5/6] Options: manually close streams Signed-off-by: Harsh Shandilya --- .../main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt index e9badec4..72429a41 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt @@ -22,9 +22,10 @@ fun BackupOption( val result = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri -> if (uri == null) return@registerForActivityResult - context.contentResolver.openOutputStream(uri)?.use { + context.contentResolver.openOutputStream(uri)?.let { coroutineScope.launch(Dispatchers.IO) { it.write(backupHandler.exportSavedPosts().toByteArray(Charsets.UTF_8)) + it.close() } } } @@ -44,9 +45,10 @@ fun RestoreOption( val result = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) return@registerForActivityResult - context.contentResolver.openInputStream(uri)?.use { + context.contentResolver.openInputStream(uri)?.let { coroutineScope.launch(Dispatchers.IO) { backupHandler.importSavedPosts(it.readBytes().toString(Charsets.UTF_8)) + it.close() } } } From b4069af1c7183f2d3b112c5ca738a45de8f78979 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sat, 3 Apr 2021 15:15:53 +0530 Subject: [PATCH 6/6] app: move ByteArray interpolation to BackupHandler Signed-off-by: Harsh Shandilya --- .../dev/msfjarvis/lobsters/data/backup/BackupHandler.kt | 8 ++++---- .../java/dev/msfjarvis/lobsters/ui/settings/Options.kt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/backup/BackupHandler.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/backup/BackupHandler.kt index 6af9b3ac..441dd670 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/data/backup/BackupHandler.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/backup/BackupHandler.kt @@ -17,15 +17,15 @@ constructor( ) { private val adapter = moshi.adapter>() - suspend fun exportSavedPosts(): String { + suspend fun exportSavedPosts(): ByteArray { val posts = withContext(Dispatchers.IO) { database.savedPostQueries.selectAllPosts().executeAsList() } - return adapter.toJson(posts) + return adapter.toJson(posts).toByteArray(Charsets.UTF_8) } - suspend fun importSavedPosts(json: String) { + suspend fun importSavedPosts(json: ByteArray) { withContext(Dispatchers.IO) { - val posts = requireNotNull(adapter.fromJson(json)) + val posts = requireNotNull(adapter.fromJson(json.toString(Charsets.UTF_8))) database.transaction { posts.forEach { database.savedPostQueries.insertOrReplacePost(it) } } } } diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt index 72429a41..2861338b 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt @@ -24,7 +24,7 @@ fun BackupOption( if (uri == null) return@registerForActivityResult context.contentResolver.openOutputStream(uri)?.let { coroutineScope.launch(Dispatchers.IO) { - it.write(backupHandler.exportSavedPosts().toByteArray(Charsets.UTF_8)) + it.write(backupHandler.exportSavedPosts()) it.close() } } @@ -47,7 +47,7 @@ fun RestoreOption( if (uri == null) return@registerForActivityResult context.contentResolver.openInputStream(uri)?.let { coroutineScope.launch(Dispatchers.IO) { - backupHandler.importSavedPosts(it.readBytes().toString(Charsets.UTF_8)) + backupHandler.importSavedPosts(it.readBytes()) it.close() } }