diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 116c7a1b..0adca8d4 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -18,7 +18,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'zulu'
- java-version: 17
+ java-version: 21
- name: Required files
run: echo $GOOGLE_SERVICES_JSON > app/google-services.json
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5dca45df..7a9e3848 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,7 +1,12 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
plugins {
+
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin)
+ alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.ksp)
+ alias(libs.plugins.baselineprofile)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.room)
alias(libs.plugins.ktlint.gradle)
@@ -9,15 +14,21 @@ plugins {
id("com.google.android.gms.oss-licenses-plugin")
}
+kotlin {
+ compilerOptions {
+ jvmTarget = JvmTarget.JVM_11
+ }
+}
+
android {
compileSdk = 36
defaultConfig {
applicationId = "com.anod.appwatcher"
- minSdk = 27
+ minSdk = 31
targetSdk = 36
- versionCode = 16900
- versionName = "1.6.9"
+ versionCode = 17000
+ versionName = "1.7.0"
}
buildFeatures {
@@ -44,13 +55,13 @@ android {
isMinifyEnabled = false
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
- proguardFile(getDefaultProguardFile("proguard-android.txt"))
+ proguardFile(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFile("proguard-project.txt")
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
- proguardFile(getDefaultProguardFile("proguard-android.txt"))
+ proguardFile(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFile("proguard-project.pro")
signingConfig = signingConfigs.getByName("release")
}
@@ -68,10 +79,6 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}
- kotlinOptions {
- jvmTarget = "11"
- }
-
packaging {
resources {
excludes += "META-INF/DEPENDENCIES"
@@ -96,20 +103,22 @@ dependencies {
ktlintRuleset(libs.ktlint.compose)
// AndroidX
implementation(libs.androidx.appcompat) // AppCompatActivity
+ implementation(libs.androidx.activity)
implementation(libs.androidx.palette)
implementation(libs.work.runtime)
implementation(libs.work.runtime.ktx)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
- implementation(libs.androidx.compose.material.icons.core)
- implementation(libs.androidx.compose.material.icons.extended)
+ implementation(libs.androidx.navigation3.ui)
+ implementation(libs.androidx.navigation3.adaptive)
implementation(libs.paging.common)
implementation(libs.paging.compose.android)
+ implementation(libs.kotlinx.serialization.json)
+
// Compose
- implementation(libs.androidx.activity.compose)
implementation(libs.runtime.tracing)
implementation(libs.ktor.client.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 99e7761f..6c16233c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,7 +22,7 @@
-
+
@@ -35,7 +35,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt
index b5b0ec9f..c99bf065 100644
--- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt
+++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt
@@ -2,25 +2,319 @@ package com.anod.appwatcher
import android.content.Context
import android.content.Intent
-import android.net.Uri
import android.os.Bundle
-import com.anod.appwatcher.watchlist.MainActivity
-import com.anod.appwatcher.watchlist.WatchListStateViewModel
+import androidx.activity.compose.setContent
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.togetherWith
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
+import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.core.net.toUri
+import androidx.core.os.bundleOf
+import androidx.navigation3.runtime.NavBackStack
+import androidx.navigation3.runtime.NavEntry
+import androidx.navigation3.runtime.NavKey
+import androidx.navigation3.runtime.entry
+import androidx.navigation3.runtime.entryProvider
+import androidx.navigation3.runtime.rememberNavBackStack
+import androidx.navigation3.ui.NavDisplay
+import com.anod.appwatcher.compose.AppTheme
+import com.anod.appwatcher.compose.BaseComposeActivity
+import com.anod.appwatcher.database.entities.Tag
+import com.anod.appwatcher.history.HistoryListScreenScene
+import com.anod.appwatcher.installed.InstalledListScreenScene
+import com.anod.appwatcher.navigation.SceneNavKey
+import com.anod.appwatcher.preferences.SettingsScreenScene
+import com.anod.appwatcher.search.SearchResultsScreenScene
+import com.anod.appwatcher.search.toViewState
+import com.anod.appwatcher.sync.SchedulesHistoryScreenScene
+import com.anod.appwatcher.tags.TagWatchListScreenScene
+import com.anod.appwatcher.userLog.UserLogScreenScene
+import com.anod.appwatcher.utils.prefs
+import com.anod.appwatcher.watchlist.DetailContent
+import com.anod.appwatcher.watchlist.EmptyBoxSmile
+import com.anod.appwatcher.watchlist.MainScreenScene
+import com.anod.appwatcher.wishlist.WishListScreenScene
+import info.anodsplace.framework.app.addMultiWindowFlags
+import org.koin.core.component.KoinComponent
-class AppWatcherActivity : MainActivity() {
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+class AppWatcherActivity : BaseComposeActivity(), KoinComponent {
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme_Main)
super.onCreate(savedInstanceState)
+
+ val elements = createInitialBackstack()
+ setContent {
+
+ val backStack = rememberNavBackStack(*elements)
+ val listDetailStrategy = rememberListDetailSceneStrategy()
+ AppTheme(
+ theme = prefs.theme,
+ transparentSystemUi = true
+ ) {
+ NavDisplay(
+ backStack = backStack,
+ onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } },
+ sceneStrategy = listDetailStrategy,
+ entryProvider = provideNavEntries(backStack),
+ transitionSpec = {
+ // Slide in from right when navigating forward
+ slideInHorizontally(
+ initialOffsetX = { it },
+ animationSpec = tween(350)
+ ) togetherWith slideOutHorizontally(
+ targetOffsetX = { -it },
+ animationSpec = tween(350)
+ )
+ },
+ popTransitionSpec = {
+ // Slide in from left when navigating back
+ slideInHorizontally(
+ initialOffsetX = { -it },
+ animationSpec = tween(350)
+ ) togetherWith slideOutHorizontally(
+ targetOffsetX = { it },
+ animationSpec = tween(350)
+ )
+ },
+ predictivePopTransitionSpec = {
+ // Slide in from left when navigating back
+ slideInHorizontally(
+ initialOffsetX = { -it },
+ animationSpec = tween(350)
+ ) togetherWith slideOutHorizontally(
+ targetOffsetX = { it },
+ animationSpec = tween(350)
+ )
+ }
+ )
+ }
+ }
+ }
+
+ private fun createInitialBackstack(): Array {
+ val extras = intent?.extras ?: bundleOf()
+ if (extras.containsKey(EXTRA_SEARCH_KEYWORD)) {
+ intent!!.extras!!.remove(EXTRA_SEARCH_KEYWORD)
+ return arrayOf(SceneNavKey.Search(
+ keyword = intent?.getStringExtra(EXTRA_SEARCH_KEYWORD) ?: "",
+ focus = intent?.getBooleanExtra(EXTRA_SEARCH_FOCUS, false) ?: false,
+ initiateSearch = intent?.getBooleanExtra(EXTRA_SEARCH_EXACT, false) ?: false,
+ isPackageSearch = intent?.getBooleanExtra(EXTRA_SEARCH_PACKAGE, false) ?: false,
+ isShareSource = intent?.getBooleanExtra(EXTRA_SEARCH_SHARE, false) ?: false,
+ ))
+ }
+ if (extras.containsKey(EXTRA_LIST_TAG_ID)) {
+ val extraTagId = extras.getInt(EXTRA_LIST_TAG_ID)
+ val extraTagColor = extras.getInt(EXTRA_LIST_TAG_COLOR, Tag.DEFAULT_COLOR)
+ // intent!!.extras!!.remove(EXTRA_DETAILS_TAG_ID)
+ return arrayOf(SceneNavKey.TagWatchList(
+ tag = Tag(
+ id = extraTagId,
+ name = "",
+ color = extraTagColor
+ )
+ ))
+ }
+ var elements = arrayOf(SceneNavKey.Main)
+ if (extras.containsKey(EXTRA_OPEN_RECENTLY_INSTALLED)) {
+ intent!!.extras!!.remove(EXTRA_OPEN_RECENTLY_INSTALLED)
+ elements += SceneNavKey.Installed(importMode = false)
+ } else if (extras.containsKey(EXTRA_LIST_TAG_ID)) {
+ val extraTagId = extras.getInt(EXTRA_LIST_TAG_ID)
+ intent!!.extras!!.remove(EXTRA_LIST_TAG_ID)
+ elements += SceneNavKey.TagWatchList(
+ tag = Tag(
+ id = extraTagId,
+ name = "",
+ color = extras.getInt(EXTRA_LIST_TAG_COLOR)
+ )
+ )
+ } else if (extras.containsKey(EXTRA_GDRIVE_SIGNIN)) {
+ intent!!.extras!!.remove(EXTRA_GDRIVE_SIGNIN)
+ elements += SceneNavKey.Settings
+ }
+ return elements
+ }
+
+ private fun provideNavEntries(backStack: NavBackStack): (NavKey) -> NavEntry = entryProvider {
+ entry(
+ metadata = ListDetailSceneStrategy.listPane(
+ sceneKey = SceneNavKey.Main,
+ detailPlaceholder = {
+ EmptyBoxSmile()
+ }
+ )
+ ) {
+ val wideLayout by foldableDevice.layout.collectAsState()
+ MainScreenScene(
+ prefs = prefs,
+ wideLayout = wideLayout,
+ navigateBack = { backStack.removeLastOrNull() },
+ navigateTo = { backStack.add(it) }
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.detailPane(sceneKey = SceneNavKey.AppDetails)
+ ) { key ->
+ DetailContent(
+ app = key.selectedApp,
+ onDismissRequest = { backStack.removeLastOrNull() },
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.listPane(
+ sceneKey = SceneNavKey.Search,
+ detailPlaceholder = {
+ EmptyBoxSmile()
+ }
+ )
+ ) { key ->
+ val wideLayout by foldableDevice.layout.collectAsState()
+ SearchResultsScreenScene(
+ initialState = key.toViewState(wideLayout),
+ navigateBack = { backStack.removeLastOrNull() },
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.extraPane(sceneKey = SceneNavKey.Settings)
+ ) {
+ SettingsScreenScene(
+ navigateBack = { backStack.removeLastOrNull() },
+ navigateTo = { backStack.add(it) }
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.extraPane(sceneKey = SceneNavKey.PurchaseHistory)
+ ) {
+ val wideLayout by foldableDevice.layout.collectAsState()
+ HistoryListScreenScene(
+ wideLayout = wideLayout,
+ navigateBack = { backStack.removeLastOrNull() }
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.extraPane(sceneKey = SceneNavKey.UserLog)
+ ) {
+ UserLogScreenScene(
+ navigateBack = { backStack.removeLastOrNull() }
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.extraPane(sceneKey = SceneNavKey.RefreshHistory)
+ ) {
+ SchedulesHistoryScreenScene(
+ navigateBack = { backStack.removeLastOrNull() }
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.listPane(
+ sceneKey = SceneNavKey.TagWatchList,
+ detailPlaceholder = {
+ EmptyBoxSmile()
+ }
+ )
+ ) { key ->
+ val wideLayout by foldableDevice.layout.collectAsState()
+ TagWatchListScreenScene(
+ tag = key.tag,
+ wideLayout = wideLayout,
+ navigateBack = { backStack.removeLastOrNull() },
+ navigateTo = { backStack.add(it) }
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.listPane(
+ sceneKey = SceneNavKey.Installed,
+ detailPlaceholder = {
+ EmptyBoxSmile()
+ }
+ )
+ ) { key ->
+ InstalledListScreenScene(
+ showAction = key.importMode,
+ navigateBack = { backStack.removeLastOrNull() },
+ )
+ }
+ entry(
+ metadata = ListDetailSceneStrategy.listPane(
+ sceneKey = SceneNavKey.WishList,
+ detailPlaceholder = {
+ EmptyBoxSmile()
+ }
+ )
+ ) {
+ val wideLayout by foldableDevice.layout.collectAsState()
+ WishListScreenScene(
+ wideLayout = wideLayout,
+ navigateBack = { backStack.removeLastOrNull() },
+ )
+ }
}
companion object {
- fun createTagShortcutIntent(tagId: Int, initialColor: Int, context: Context) = Intent(context, AppWatcherActivity::class.java).apply {
+ const val EXTRA_SEARCH_KEYWORD = "search.keyword"
+ const val EXTRA_SEARCH_EXACT = "search.exact"
+ const val EXTRA_SEARCH_SHARE = "search.share"
+ const val EXTRA_SEARCH_FOCUS = "search.focus"
+ const val EXTRA_SEARCH_PACKAGE = "search.package"
+
+ const val EXTRA_LIST_TAG = "extra_tag"
+ const val EXTRA_LIST_TAG_ID = "tag_id"
+ const val EXTRA_LIST_TAG_COLOR = "tag_color"
+
+ const val EXTRA_FROM_NOTIFICATION = "list.extra_noti"
+ const val EXTRA_EXPAND_SEARCH = "list.expand_search"
+
+ const val EXTRA_OPEN_RECENTLY_INSTALLED = "open_recently_installed"
+ const val EXTRA_GDRIVE_SIGNIN = "gdrive.signin"
+
+ const val ARG_FILTER = "filter"
+ const val ARG_SORT = "sort"
+ const val ARG_TAG = "tag"
+ const val ARG_SHOW_ACTION = "showAction"
+
+ fun tagShortcutIntent(tagId: Int, initialColor: Int, context: Context) = Intent(context, AppWatcherActivity::class.java).apply {
action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- data = Uri.parse("com.anod.appwatcher://tags/$tagId?color=$initialColor")
- putExtra(WatchListStateViewModel.EXTRA_TAG_ID, tagId)
- putExtra(WatchListStateViewModel.EXTRA_TAG_COLOR, initialColor)
+ data = "com.anod.appwatcher://tags/$tagId?color=$initialColor".toUri()
+ putExtra(EXTRA_LIST_TAG_ID, tagId)
+ putExtra(EXTRA_LIST_TAG_COLOR, initialColor)
}
+
+ fun searchIntent(
+ context: Context,
+ keyword: String,
+ focus: Boolean,
+ initiateSearch: Boolean = false
+ ): Intent = Intent(context, AppWatcherActivity::class.java).apply {
+ putExtra(EXTRA_SEARCH_KEYWORD, keyword)
+ putExtra(EXTRA_SEARCH_FOCUS, focus)
+ putExtra(EXTRA_SEARCH_EXACT, initiateSearch)
+ }
+
+ fun tagIntent(tag: Tag, context: Context) = Intent(context, AppWatcherActivity::class.java).apply {
+ putExtra(EXTRA_LIST_TAG, tag)
+ addMultiWindowFlags(context)
+ }
+
+ fun gDriveSignInIntent(context: Context) = Intent(context, AppWatcherActivity::class.java).apply {
+ putExtra(EXTRA_GDRIVE_SIGNIN, true)
+ addMultiWindowFlags(context)
+ }
+
+ private fun installedIntent(sortId: Int, showImportAction: Boolean, context: Context): Intent {
+ return Intent(context, AppWatcherActivity::class.java).apply {
+ putExtra(ARG_SORT, sortId)
+ putExtra(ARG_SHOW_ACTION, showImportAction)
+ }
+ }
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherApplication.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherApplication.kt
index 4fdaa8f1..d099bc6a 100644
--- a/app/src/main/java/com/anod/appwatcher/AppWatcherApplication.kt
+++ b/app/src/main/java/com/anod/appwatcher/AppWatcherApplication.kt
@@ -45,7 +45,7 @@ class AppWatcherApplication : Application(), AppLog.Listener, ApplicationInstanc
override fun onCreate() {
super.onCreate()
- if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectAll()
diff --git a/app/src/main/java/com/anod/appwatcher/MarketSearchActivity.kt b/app/src/main/java/com/anod/appwatcher/MarketSearchActivity.kt
deleted file mode 100644
index efca4aeb..00000000
--- a/app/src/main/java/com/anod/appwatcher/MarketSearchActivity.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.anod.appwatcher
-
-import android.content.Context
-import android.content.Intent
-import com.anod.appwatcher.search.SearchComposeActivity
-
-class MarketSearchActivity : SearchComposeActivity() {
-
- companion object {
- fun intent(
- context: Context,
- keyword: String,
- focus: Boolean,
- initiateSearch: Boolean = false
- ): Intent = Intent(context, MarketSearchActivity::class.java).apply {
- putExtra(EXTRA_KEYWORD, keyword)
- putExtra(EXTRA_FOCUS, focus)
- putExtra(EXTRA_EXACT, initiateSearch)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/SettingsActivity.kt b/app/src/main/java/com/anod/appwatcher/SettingsActivity.kt
deleted file mode 100644
index c3f9aa53..00000000
--- a/app/src/main/java/com/anod/appwatcher/SettingsActivity.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.anod.appwatcher
-
-class SettingsActivity : com.anod.appwatcher.preferences.SettingsActivity()
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/ShareRecieverActivity.kt b/app/src/main/java/com/anod/appwatcher/ShareRecieverActivity.kt
index 96ac3657..f2f76a7c 100644
--- a/app/src/main/java/com/anod/appwatcher/ShareRecieverActivity.kt
+++ b/app/src/main/java/com/anod/appwatcher/ShareRecieverActivity.kt
@@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Intent
import android.net.UrlQuerySanitizer
import android.os.Bundle
-import com.anod.appwatcher.search.SearchComposeActivity
class ShareRecieverActivity : Activity() {
@@ -13,15 +12,15 @@ class ShareRecieverActivity : Activity() {
val intent = intent
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
- val searchIntent = Intent(this, MarketSearchActivity::class.java)
+ val searchIntent = Intent(this, AppWatcherActivity::class.java)
var fallback = true
if (text != null && text.startsWith(URL_PLAYSTORE)) {
val sanitizer = UrlQuerySanitizer(text)
val id = sanitizer.getValue("id")
if (id != null) {
- searchIntent.putExtra(SearchComposeActivity.EXTRA_PACKAGE, true)
- searchIntent.putExtra(SearchComposeActivity.EXTRA_KEYWORD, id)
- searchIntent.putExtra(SearchComposeActivity.EXTRA_EXACT, true)
+ searchIntent.putExtra(AppWatcherActivity.EXTRA_SEARCH_PACKAGE, true)
+ searchIntent.putExtra(AppWatcherActivity.EXTRA_SEARCH_KEYWORD, id)
+ searchIntent.putExtra(AppWatcherActivity.EXTRA_SEARCH_EXACT, true)
fallback = false
}
}
@@ -29,15 +28,15 @@ class ShareRecieverActivity : Activity() {
if (fallback) {
val title = intent.getStringExtra(Intent.EXTRA_TITLE)
if (title != null && title != "") {
- searchIntent.putExtra(SearchComposeActivity.EXTRA_KEYWORD, title)
+ searchIntent.putExtra(AppWatcherActivity.EXTRA_SEARCH_KEYWORD, title)
} else if (text != null && text != "") {
- searchIntent.putExtra(SearchComposeActivity.EXTRA_KEYWORD, text)
+ searchIntent.putExtra(AppWatcherActivity.EXTRA_SEARCH_KEYWORD, text)
} else {
- searchIntent.putExtra(SearchComposeActivity.EXTRA_KEYWORD, "")
+ searchIntent.putExtra(AppWatcherActivity.EXTRA_SEARCH_KEYWORD, "")
}
- searchIntent.putExtra(SearchComposeActivity.EXTRA_EXACT, false)
+ searchIntent.putExtra(AppWatcherActivity.EXTRA_SEARCH_EXACT, false)
}
- searchIntent.putExtra(SearchComposeActivity.EXTRA_SHARE, true)
+ searchIntent.putExtra(AppWatcherActivity.EXTRA_SEARCH_SHARE, true)
startActivity(searchIntent)
finish()
}
diff --git a/app/src/main/java/com/anod/appwatcher/accounts/AccountSelectionDialog.kt b/app/src/main/java/com/anod/appwatcher/accounts/AccountSelectionDialog.kt
deleted file mode 100644
index 678465c9..00000000
--- a/app/src/main/java/com/anod/appwatcher/accounts/AccountSelectionDialog.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.anod.appwatcher.accounts
-
-import android.accounts.Account
-import android.accounts.AccountManager
-import android.app.Activity
-import android.content.Intent
-import androidx.activity.ComponentActivity
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
-import com.anod.appwatcher.preferences.Preferences
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.launch
-
-sealed interface AccountSelectionResult {
- class Success(val account: Account) : AccountSelectionResult
- data object Canceled : AccountSelectionResult
- class Error(val errorMessage: String) : AccountSelectionResult
-}
-
-/**
- * @author alex
- * *
- * @date 9/17/13
- */
-class AccountSelectionDialog(
- private val activity: ComponentActivity,
- private val preferences: Preferences
-) {
-
- private val chooseAccount = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- onActivityResult(result.resultCode, result.data)
- }
-
- val accountSelected = MutableSharedFlow()
-
- fun show() {
- val intent = AccountManager.newChooseAccountIntent(
- preferences.account?.toAndroidAccount(),
- null,
- arrayOf(AuthTokenBlocking.ACCOUNT_TYPE),
- null,
- null,
- null,
- null)
- chooseAccount.launch(intent)
- }
-
- private fun onActivityResult(resultCode: Int, data: Intent?) {
- if (resultCode == Activity.RESULT_CANCELED) {
- activity.lifecycleScope.launch {
- accountSelected.emit(AccountSelectionResult.Canceled)
- }
- return
- }
-
- if (resultCode == Activity.RESULT_OK && data != null) {
- val name = data.extras?.getString(AccountManager.KEY_ACCOUNT_NAME, "") ?: ""
- val type = data.extras?.getString(AccountManager.KEY_ACCOUNT_TYPE, "") ?: ""
- if (name.isNotBlank() && type.isNotBlank()) {
- val account = Account(name, type)
- activity.lifecycleScope.launch {
- accountSelected.emit(AccountSelectionResult.Success(account))
- }
- return
- }
- }
- val errorMessage = data?.extras?.getString(AccountManager.KEY_ERROR_MESSAGE, null)
- activity.lifecycleScope.launch {
- accountSelected.emit(AccountSelectionResult.Error(errorMessage ?: "Cannot retrieve account"))
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/accounts/AccountSelectionRequest.kt b/app/src/main/java/com/anod/appwatcher/accounts/AccountSelectionRequest.kt
new file mode 100644
index 00000000..b8cff7f5
--- /dev/null
+++ b/app/src/main/java/com/anod/appwatcher/accounts/AccountSelectionRequest.kt
@@ -0,0 +1,47 @@
+package com.anod.appwatcher.accounts
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+
+sealed interface AccountSelectionResult {
+ class Success(val account: Account) : AccountSelectionResult
+ data object Canceled : AccountSelectionResult
+ class Error(val errorMessage: String) : AccountSelectionResult
+}
+
+class AccountSelectionRequest : ActivityResultContract() {
+
+ override fun createIntent(context: Context, input: Account?): Intent {
+ val intent = AccountManager.newChooseAccountIntent(
+ input,
+ null,
+ arrayOf(AuthTokenBlocking.ACCOUNT_TYPE),
+ null,
+ null,
+ null,
+ null
+ )
+ return intent
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): AccountSelectionResult {
+ if (resultCode == Activity.RESULT_CANCELED) {
+ return AccountSelectionResult.Canceled
+ }
+
+ if (resultCode == Activity.RESULT_OK && intent != null) {
+ val name = intent.extras?.getString(AccountManager.KEY_ACCOUNT_NAME, "") ?: ""
+ val type = intent.extras?.getString(AccountManager.KEY_ACCOUNT_TYPE, "") ?: ""
+ if (name.isNotBlank() && type.isNotBlank()) {
+ val account = Account(name, type)
+ return AccountSelectionResult.Success(account)
+ }
+ }
+ val errorMessage = intent?.extras?.getString(AccountManager.KEY_ERROR_MESSAGE, null)
+ return AccountSelectionResult.Error(errorMessage ?: "Cannot retrieve account")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/accounts/AuthTokenBlocking.kt b/app/src/main/java/com/anod/appwatcher/accounts/AuthTokenBlocking.kt
index 84462fc6..69c958ef 100644
--- a/app/src/main/java/com/anod/appwatcher/accounts/AuthTokenBlocking.kt
+++ b/app/src/main/java/com/anod/appwatcher/accounts/AuthTokenBlocking.kt
@@ -7,11 +7,11 @@ import android.accounts.OperationCanceledException
import android.content.Intent
import info.anodsplace.applog.AppLog
import info.anodsplace.context.ApplicationContext
-import java.io.IOException
-import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
+import java.io.IOException
+import java.util.concurrent.TimeUnit
class AuthTokenStartIntent(val intent: Intent) : RuntimeException("getAuthToken finished with intent: $intent")
@@ -19,6 +19,7 @@ sealed interface CheckTokenError {
class Unknown(val e: Exception) : CheckTokenError
class RequiresInteraction(val intent: Intent) : CheckTokenError
object NoToken : CheckTokenError
+ object NoAccount : CheckTokenError
}
sealed interface CheckTokenResult {
@@ -44,7 +45,8 @@ class AuthTokenBlocking(context: ApplicationContext) {
private val accountManager: AccountManager = AccountManager.get(context.actual)
private var lastUpdated = 0L
- suspend fun checkToken(account: Account): CheckTokenResult {
+ suspend fun checkToken(account: Account?): CheckTokenResult {
+ val account = account ?: return CheckTokenResult.Error(CheckTokenError.NoAccount)
if (isFresh) {
return CheckTokenResult.Success(invalidated = false)
}
diff --git a/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSignIn.kt b/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSignIn.kt
index 457267e7..11117635 100644
--- a/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSignIn.kt
+++ b/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSignIn.kt
@@ -1,21 +1,19 @@
package com.anod.appwatcher.backup.gdrive
+import android.accounts.Account
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
+import com.anod.appwatcher.AppWatcherActivity
import com.anod.appwatcher.R
-import com.anod.appwatcher.SettingsActivity
import com.anod.appwatcher.sync.SyncNotification
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.auth.api.signin.GoogleSignInStatusCodes
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.Scope
-import com.google.android.gms.tasks.Task
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.client.http.HttpRequestInitializer
import com.google.api.services.drive.DriveScopes
@@ -23,11 +21,12 @@ import info.anodsplace.applog.AppLog
import info.anodsplace.context.ApplicationContext
import info.anodsplace.notification.NotificationManager
import info.anodsplace.playservices.GoogleSignInConnect
+import org.koin.java.KoinJavaComponent
import java.util.Collections
import java.util.concurrent.ExecutionException
import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
-import org.koin.java.KoinJavaComponent
internal fun createGDriveSignInOptions(): GoogleSignInOptions {
return GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
@@ -36,19 +35,15 @@ internal fun createGDriveSignInOptions(): GoogleSignInOptions {
.build()
}
-internal fun createCredentials(context: Context, googleAccount: GoogleSignInAccount): HttpRequestInitializer {
+internal fun createCredentials(context: Context, googleAccount: Account?): HttpRequestInitializer {
return GoogleAccountCredential
.usingOAuth2(context, Collections.singleton(DriveScopes.DRIVE_APPDATA))
- .setSelectedAccount(googleAccount.account)
+ .setSelectedAccount(googleAccount)
}
-interface ResultListener {
- fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
-}
+class GDriveSignIn(private val context: ApplicationContext) {
-class GDriveSignIn(private val activity: Activity, private val listener: Listener) : ResultListener {
-
- private val driveConnect by lazy { GoogleSignInConnect(activity, createGDriveSignInOptions()) }
+ private val driveConnect by lazy { GoogleSignInConnect(context, createGDriveSignInOptions()) }
companion object {
const val RESULT_CODE_GDRIVE_SIGN_IN = 123
@@ -65,27 +60,29 @@ class GDriveSignIn(private val activity: Activity, private val listener: Listene
val notificationManager = KoinJavaComponent.getKoin().get()
notificationManager.notify(SyncNotification.GMS_NOTIFICATION_ID, notification)
}
- }
- interface Listener {
- fun onGDriveLoginSuccess(googleSignInAccount: GoogleSignInAccount)
- fun onGDriveLoginError(errorCode: Int)
+ fun getLastSignedInAccount(context: Context): Account? {
+ return GoogleSignIn.getLastSignedInAccount(context)?.account
+ }
}
- fun signIn() {
+ class GoogleSignInRequestException(val intent: Intent, val resultCode: Int) : Throwable()
+ class GoogleSignInFailedException(val resultCode: Int) : Throwable()
+
+ suspend fun signIn() = suspendCoroutine { continuation ->
driveConnect.connect(object : GoogleSignInConnect.Result {
- override fun onSuccess(account: GoogleSignInAccount, client: GoogleSignInClient) {
- listener.onGDriveLoginSuccess(account)
+ override fun onSuccess(account: Account) {
+ continuation.resume(account)
}
- override fun onError(errorCode: Int, client: GoogleSignInClient) {
- AppLog.e("Silent sign in failed with code $errorCode (${GoogleSignInStatusCodes.getStatusCodeString(errorCode)}). starting signIn intent")
- activity.startActivityForResult(client.signInIntent, RESULT_CODE_GDRIVE_SIGN_IN)
+ override fun onError(errorCode: Int, errorMessage: String, signInIntent: Intent) {
+ AppLog.e("Silent sign in failed with code $errorCode ($errorMessage). starting signIn intent")
+ continuation.resumeWithException(GoogleSignInRequestException(signInIntent, RESULT_CODE_GDRIVE_SIGN_IN))
}
})
}
- suspend fun signOut() = suspendCoroutine { continuation ->
+ suspend fun signOut() = suspendCoroutine { continuation ->
driveConnect.disconnect(object : GoogleSignInConnect.SignOutResult {
override fun onResult() {
continuation.resume(Unit)
@@ -93,45 +90,37 @@ class GDriveSignIn(private val activity: Activity, private val listener: Listene
})
}
- fun requestEmail(lastSignedAccount: GoogleSignInAccount) {
- GoogleSignIn.requestPermissions(activity, RESULT_CODE_GDRIVE_SIGN_IN, lastSignedAccount, Scope("email"))
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ suspend fun onActivityResult(resultCode: Int, data: Intent?) = suspendCoroutine { continuation ->
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
- if (requestCode == RESULT_CODE_GDRIVE_SIGN_IN) {
+ if (resultCode == Activity.RESULT_OK && data?.extras != null) {
// The Task returned from this call is always completed, no need to attach
// a listener.
- val task = GoogleSignIn.getSignedInAccountFromIntent(data)
- handleSignInResult(task)
- } else if (requestCode == RESULT_CODE_GDRIVE_EXCEPTION) {
+ val completedTask = GoogleSignIn.getSignedInAccountFromIntent(data)
+ try {
+ val account = completedTask.getResult(ApiException::class.java)!!
+ // Signed in successfully, show authenticated UI.
+ continuation.resume(account)
+ } catch (e: ApiException) {
+ // The ApiException status code indicates the detailed failure reason.
+ // Please refer to the GoogleSignInStatusCodes class reference for more information.
+ AppLog.e(e)
+ continuation.resumeWithException(GoogleSignInFailedException(e.statusCode))
+ }
+ } else {
+ continuation.resumeWithException(GoogleSignInFailedException(resultCode))
// Nothing?
}
}
-
- private fun handleSignInResult(completedTask: Task) {
- try {
- val account = completedTask.getResult(ApiException::class.java)!!
-
- // Signed in successfully, show authenticated UI.
- listener.onGDriveLoginSuccess(account)
- } catch (e: ApiException) {
- // The ApiException status code indicates the detailed failure reason.
- // Please refer to the GoogleSignInStatusCodes class reference for more information.
- AppLog.e(e)
- listener.onGDriveLoginError(e.statusCode)
- }
- }
}
class GDriveSilentSignIn(private val context: ApplicationContext) {
private val driveConnect by lazy { GoogleSignInConnect(context, createGDriveSignInOptions()) }
- fun signInLocked(): GoogleSignInAccount {
+ fun signInLocked(): Account {
val lastSignedInAccount = GoogleSignIn.getLastSignedInAccount(context.actual)
- if (lastSignedInAccount != null) {
- return lastSignedInAccount
+ if (lastSignedInAccount?.account != null) {
+ return lastSignedInAccount.account!!
}
try {
@@ -140,7 +129,7 @@ class GDriveSilentSignIn(private val context: ApplicationContext) {
val errorCode = e.statusCode
AppLog.e("Silent sign in failed with code $errorCode (${GoogleSignInStatusCodes.getStatusCodeString(errorCode)}). starting signIn intent")
if (errorCode == GoogleSignInStatusCodes.SIGN_IN_REQUIRED) {
- val settingActivity = Intent(context.actual, SettingsActivity::class.java)
+ val settingActivity = AppWatcherActivity.gDriveSignInIntent(context.actual)
settingActivity.flags = Intent.FLAG_ACTIVITY_NEW_TASK
GDriveSignIn.showResolutionNotification(
PendingIntent.getActivity(context.actual, 0, settingActivity, PendingIntent.FLAG_IMMUTABLE), context)
diff --git a/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt b/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt
index 77c817ce..97647ee0 100644
--- a/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt
+++ b/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt
@@ -1,5 +1,6 @@
package com.anod.appwatcher.backup.gdrive
+import android.accounts.Account
import android.text.format.Formatter
import com.anod.appwatcher.backup.DbJsonReader
import com.anod.appwatcher.backup.DbJsonWriter
@@ -10,21 +11,19 @@ import com.anod.appwatcher.database.TagsTable
import com.anod.appwatcher.database.entities.App
import com.anod.appwatcher.database.entities.Tag
import com.google.android.gms.auth.UserRecoverableAuthException
-import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import info.anodsplace.applog.AppLog
-import info.anodsplace.context.ApplicationContext
-import java.io.BufferedReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
+import java.io.BufferedReader
/**
* @author alex
* *
* @date 2014-11-15
*/
-class GDriveSync(private val googleAccount: GoogleSignInAccount, private val context: info.anodsplace.context.ApplicationContext, private val database: AppsDatabase) {
+class GDriveSync(private val googleAccount: Account, private val context: info.anodsplace.context.ApplicationContext, private val database: AppsDatabase) {
class SyncError(val error: UserRecoverableAuthException?, cause: Exception) : Exception(cause.message, cause)
diff --git a/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveUpload.kt b/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveUpload.kt
index 4ef9c79d..0dcddde7 100644
--- a/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveUpload.kt
+++ b/app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveUpload.kt
@@ -1,10 +1,10 @@
package com.anod.appwatcher.backup.gdrive
+import android.accounts.Account
import android.text.format.Formatter
import com.anod.appwatcher.backup.DbJsonWriter
import com.anod.appwatcher.database.AppTagsTable
import com.anod.appwatcher.database.AppsDatabase
-import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import info.anodsplace.applog.AppLog
import info.anodsplace.context.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -16,7 +16,7 @@ import kotlinx.coroutines.withContext
* @author Alex Gavrishev
* @date 26/06/2017
*/
-class GDriveUpload(private val googleAccount: GoogleSignInAccount, private val context: info.anodsplace.context.ApplicationContext, private val database: AppsDatabase) {
+class GDriveUpload(private val googleAccount: Account, private val context: ApplicationContext, private val database: AppsDatabase) {
@Throws(Exception::class)
suspend fun doUploadInBackground() {
diff --git a/app/src/main/java/com/anod/appwatcher/backup/gdrive/UploadService.kt b/app/src/main/java/com/anod/appwatcher/backup/gdrive/UploadService.kt
index 091f5568..d760046b 100644
--- a/app/src/main/java/com/anod/appwatcher/backup/gdrive/UploadService.kt
+++ b/app/src/main/java/com/anod/appwatcher/backup/gdrive/UploadService.kt
@@ -11,14 +11,13 @@ import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
-import com.anod.appwatcher.SettingsActivity
+import com.anod.appwatcher.AppWatcherActivity
import com.anod.appwatcher.utils.prefs
-import com.google.android.gms.auth.api.signin.GoogleSignIn
import info.anodsplace.applog.AppLog
-import java.util.concurrent.TimeUnit
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.parameter.parametersOf
+import java.util.concurrent.TimeUnit
/**
* @author Alex Gavrishev
@@ -55,7 +54,7 @@ class UploadService(appContext: Context, params: WorkerParameters) : CoroutineWo
AppLog.d("Scheduled call executed. Id: $id")
AppLog.d("DriveSync perform upload")
- val googleAccount = GoogleSignIn.getLastSignedInAccount(applicationContext)
+ val googleAccount = GDriveSignIn.getLastSignedInAccount(applicationContext)
if (googleAccount == null) {
AppLog.e("Account is null")
return Result.failure()
@@ -67,7 +66,7 @@ class UploadService(appContext: Context, params: WorkerParameters) : CoroutineWo
} catch (e: Exception) {
AppLog.e("UploadService::doWork - ${e.message}", e)
DriveService.extractUserRecoverableException(e)?.let {
- val settingActivity = Intent(applicationContext, SettingsActivity::class.java).apply {
+ val settingActivity = AppWatcherActivity.gDriveSignInIntent(applicationContext).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
GDriveSignIn.showResolutionNotification(
diff --git a/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt
index 2452424d..9dc54fa2 100644
--- a/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt
+++ b/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt
@@ -2,6 +2,7 @@ package com.anod.appwatcher.compose
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.activity.enableEdgeToEdge
import info.anodsplace.framework.app.FoldableDevice
abstract class BaseComposeActivity : ComponentActivity() {
@@ -9,6 +10,14 @@ abstract class BaseComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
foldableDevice = FoldableDevice.create(this)
+ setEdgeToEdgeConfig()
super.onCreate(savedInstanceState)
}
+}
+
+fun ComponentActivity.setEdgeToEdgeConfig() {
+ enableEdgeToEdge()
+ // Force the 3-button navigation bar to be transparent
+ // See: https://developer.android.com/develop/ui/views/layout/edge-to-edge#create-transparent
+ window.isNavigationBarContrastEnforced = false
}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/compose/MainDetailsScreen.kt b/app/src/main/java/com/anod/appwatcher/compose/MainDetailsScreen.kt
deleted file mode 100644
index 68147dd1..00000000
--- a/app/src/main/java/com/anod/appwatcher/compose/MainDetailsScreen.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.anod.appwatcher.compose
-
-import android.graphics.Rect
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.tooling.preview.Devices
-import androidx.compose.ui.tooling.preview.Preview
-import com.anod.appwatcher.watchlist.DetailContent
-import info.anodsplace.framework.app.FoldableDeviceLayout
-
-@Composable
-fun MainDetailScreen(wideLayout: FoldableDeviceLayout, main: @Composable () -> Unit, detail: @Composable () -> Unit) {
- Row(
- modifier = Modifier.fillMaxSize()
- ) {
- Box(modifier = Modifier
- .weight(1f)
- .fillMaxHeight()) {
- main()
- }
- val hingeWidth = wideLayout.hinge.width()
- if (hingeWidth > 0) {
- val widthInDp = with(LocalDensity.current) { hingeWidth.toDp() }
- Spacer(modifier = Modifier
- .width(widthInDp)
- .fillMaxHeight())
- }
- Box(modifier = Modifier
- .weight(1f)
- .fillMaxHeight()) {
- detail()
- }
- }
-}
-
-@Preview(device = Devices.FOLDABLE)
-@Composable
-private fun MainDetailScreenPreview() {
- AppTheme {
- MainDetailScreen(
- wideLayout = FoldableDeviceLayout(isWideLayout = true, hinge = Rect(0, 0, 80, 0)),
- main = { DetailContent(app = null, onDismissRequest = {}, onCommonActivityAction = {}) },
- detail = { DetailContent(app = null, onDismissRequest = {}, onCommonActivityAction = {}) },
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/compose/SearchTopBar.kt b/app/src/main/java/com/anod/appwatcher/compose/SearchTopBar.kt
index e52753da..061ba71f 100644
--- a/app/src/main/java/com/anod/appwatcher/compose/SearchTopBar.kt
+++ b/app/src/main/java/com/anod/appwatcher/compose/SearchTopBar.kt
@@ -91,7 +91,6 @@ fun SearchTopBar(
} else {
onNavigation()
}
- onNavigation()
}
SearchTopBarEvent.SearchAction -> {
showSearchView = true
diff --git a/app/src/main/java/com/anod/appwatcher/compose/Theme.kt b/app/src/main/java/com/anod/appwatcher/compose/Theme.kt
index 2c521e21..f482fb5b 100644
--- a/app/src/main/java/com/anod/appwatcher/compose/Theme.kt
+++ b/app/src/main/java/com/anod/appwatcher/compose/Theme.kt
@@ -1,18 +1,14 @@
package com.anod.appwatcher.compose
-import android.os.Build
import android.view.Window
-import androidx.annotation.RequiresApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
-import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
@@ -32,116 +28,21 @@ import info.anodsplace.framework.app.findWindow
private val AppTypography = Typography()
val Amber800 = Color(0xFFFF8F00)
-private val LightThemeColors = lightColorScheme(
- primary = Color(0xFF2196F3),
- onPrimary = Color(0xFFffffff),
- primaryContainer = Color(0xFFbde9ff),
- onPrimaryContainer = Color(0xFF001f2a),
- secondary = Color(0xFF005db7),
- onSecondary = Color(0xFFffffff),
- secondaryContainer = Color(0xFFd6e3ff),
- onSecondaryContainer = Color(0xFF001b3d),
- tertiary = Color(0xFF2b5bb5),
- onTertiary = Color(0xFFffffff),
- tertiaryContainer = Color(0xFFd9e2ff),
- onTertiaryContainer = Color(0xFF001945),
- error = Color(0xFFba1a1a),
- onError = Color(0xFFffffff),
- errorContainer = Color(0xFFffdad6),
- onErrorContainer = Color(0xFF410002),
-
- background = Color(0xFFfafcff),
- onBackground = Color(0xFF001f2a),
-
- surface = Color(0xFFfafcff),
- onSurface = Color(0xFF001f2a),
-
- surfaceVariant = Color(0xFFdce4e9),
- onSurfaceVariant = Color(0xFF40484c),
- outline = Color(0xFF70787d)
-)
-
-private val DarkSurface = Color(0xFF263238)
-private val DarkThemeColors = darkColorScheme(
- primary = Color(0xFF67d3ff),
- onPrimary = Color(0xFF003546),
- primaryContainer = Color(0xFF004d64),
- onPrimaryContainer = Color(0xFFbde9ff),
- secondary = Color(0xFF8ccdff),
- onSecondary = Color(0xFF00344e),
- secondaryContainer = Color(0xFF004b6f),
- onSecondaryContainer = Color(0xFFcae6ff),
- tertiary = Color(0xFF8ecdff),
- onTertiary = Color(0xFFffffff),
- tertiaryContainer = Color(0xFFcae6ff),
- onTertiaryContainer = Color(0xFF001e30),
- error = Color(0xFFffb4ab),
- onError = Color(0xFF690005),
- errorContainer = Color(0xFF93000a),
- onErrorContainer = Color(0xFFffdad6),
-
- background = DarkSurface,
- onBackground = Color(0xFFbde9ff),
-
- surface = DarkSurface,
- onSurface = Color(0xFFffffff),
-
- surfaceVariant = Color(0xFF40484c),
- onSurfaceVariant = Color(0xFFc0c8cd),
- outline = Color(0xFF8a9297)
-)
-
-private val BlackThemeColors = darkColorScheme(
- primary = Color(0xFF67d3ff),
- onPrimary = Color(0xFF003546),
- primaryContainer = Color(0xFF004d64),
- onPrimaryContainer = Color(0xFFbde9ff),
- secondary = Color(0xFF8ccdff),
- onSecondary = Color(0xFF00344e),
- secondaryContainer = Color(0xFF004b6f),
- onSecondaryContainer = Color(0xFFcae6ff),
- tertiary = Color(0xFF8ecdff),
- onTertiary = Color(0xFFffffff),
- tertiaryContainer = Color(0xFFcae6ff),
- onTertiaryContainer = Color(0xFF001e30),
- error = Color(0xFFffb4ab),
- onError = Color(0xFF690005),
- errorContainer = Color(0xFF93000a),
- onErrorContainer = Color(0xFFffdad6),
-
- background = Color.Black,
- onBackground = Color(0xFFbde9ff),
-
- surface = Color.Black,
- onSurface = Color(0xFFffffff),
-
- surfaceVariant = Color(0xFF40484c),
- onSurfaceVariant = Color(0xFFc0c8cd),
- outline = Color(0xFF8a9297)
-)
-
val AppShapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(8.dp)
)
-fun supportsDynamic(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
-
-@RequiresApi(Build.VERSION_CODES.S)
@Composable
-fun darkTheme(theme: Int, supportsDynamic: Boolean): ColorScheme {
+fun darkTheme(theme: Int): ColorScheme {
return if (theme == Preferences.THEME_BLACK) {
- if (supportsDynamic) {
- dynamicDarkColorScheme(LocalContext.current).copy(
- background = Color.Black,
- surface = Color.Black,
- )
- } else {
- BlackThemeColors
- }
+ dynamicDarkColorScheme(LocalContext.current).copy(
+ background = Color.Black,
+ surface = Color.Black,
+ )
} else {
- if (supportsDynamic) dynamicDarkColorScheme(LocalContext.current) else DarkThemeColors
+ dynamicDarkColorScheme(LocalContext.current)
}
}
@@ -155,11 +56,7 @@ fun AppTheme(
transparentSystemUi: Boolean = false,
content: @Composable () -> Unit
) {
- var colorScheme = if (supportsDynamic()) {
- if (darkTheme) darkTheme(theme, supportsDynamic = true) else dynamicLightColorScheme(LocalContext.current)
- } else {
- if (darkTheme) darkTheme(theme, supportsDynamic = false) else LightThemeColors
- }
+ var colorScheme = if (darkTheme) darkTheme(theme) else dynamicLightColorScheme(LocalContext.current)
var statusBarColor = colorScheme.surface
var isAppearanceLightStatusBars = !darkTheme
diff --git a/app/src/main/java/com/anod/appwatcher/compose/ViewModelStoreOwner.kt b/app/src/main/java/com/anod/appwatcher/compose/ViewModelStoreOwner.kt
index df87d526..ad0c3560 100644
--- a/app/src/main/java/com/anod/appwatcher/compose/ViewModelStoreOwner.kt
+++ b/app/src/main/java/com/anod/appwatcher/compose/ViewModelStoreOwner.kt
@@ -18,7 +18,7 @@ class ComposableViewModelStoreOwner : ViewModelStoreOwner {
}
@Composable
-fun rememberViwModeStoreOwner(): ViewModelStoreOwner {
+fun rememberViewModeStoreOwner(): ViewModelStoreOwner {
val owner = remember { ComposableViewModelStoreOwner() }
DisposableEffect(key1 = owner) {
onDispose {
diff --git a/app/src/main/java/com/anod/appwatcher/database/AppsDatabase.kt b/app/src/main/java/com/anod/appwatcher/database/AppsDatabase.kt
index 5334e0f8..eb015456 100644
--- a/app/src/main/java/com/anod/appwatcher/database/AppsDatabase.kt
+++ b/app/src/main/java/com/anod/appwatcher/database/AppsDatabase.kt
@@ -61,14 +61,14 @@ abstract class AppsDatabase : RoomDatabase() {
val dbName = if (BuildConfig.DEBUG) "app_watcher.db" else "app_watcher"
private val MIGRATION_17_18 = object : Migration(17, 18) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("ALTER TABLE ${SchedulesTable.TABLE} ADD COLUMN ${SchedulesTable.Columns.NOTIFIED} INTEGER NOT NULL DEFAULT 0")
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("ALTER TABLE ${SchedulesTable.TABLE} ADD COLUMN ${SchedulesTable.Columns.NOTIFIED} INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATION_16_17 = object : Migration(16, 17) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL(
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL(
"CREATE TABLE IF NOT EXISTS `${SchedulesTable.TABLE}` (" +
"`_id` INTEGER NOT NULL, " +
"`start` INTEGER NOT NULL, " +
@@ -83,24 +83,24 @@ abstract class AppsDatabase : RoomDatabase() {
}
private val MIGRATION_15_16 = object : Migration(15, 16) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("ALTER TABLE " + ChangelogTable.TABLE + " ADD COLUMN " + ChangelogTable.Columns.NO_NEW_DETAILS + " INTEGER NOT NULL DEFAULT 0")
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("ALTER TABLE " + ChangelogTable.TABLE + " ADD COLUMN " + ChangelogTable.Columns.NO_NEW_DETAILS + " INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATION_14_15 = object : Migration(14, 15) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("DELETE FROM app_tags WHERE _ID NOT IN ( " +
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("DELETE FROM app_tags WHERE _ID NOT IN ( " +
"SELECT MAX(_ID) " +
"FROM app_tags " +
"GROUP BY app_id, tags_id)")
- database.execSQL("CREATE UNIQUE INDEX `index_app_tags_app_id_tags_id` ON `app_tags` (`app_id`, `tags_id`)")
+ db.execSQL("CREATE UNIQUE INDEX `index_app_tags_app_id_tags_id` ON `app_tags` (`app_id`, `tags_id`)")
}
}
private val MIGRATION_9_11 = object : Migration(9, 11) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("CREATE TABLE IF NOT EXISTS `changelog` (" +
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS `changelog` (" +
"`_id` INTEGER NOT NULL, " +
"`app_id` TEXT NOT NULL, " +
"`code` INTEGER NOT NULL, " +
@@ -111,9 +111,9 @@ abstract class AppsDatabase : RoomDatabase() {
}
private val MIGRATION_11_12 = object : Migration(11, 12) {
- override fun migrate(database: SupportSQLiteDatabase) {
+ override fun migrate(db: SupportSQLiteDatabase) {
try {
- database.execSQL("ALTER TABLE " + ChangelogTable.TABLE + " ADD COLUMN " + ChangelogTable.Columns.UPLOAD_DATE + " TEXT")
+ db.execSQL("ALTER TABLE " + ChangelogTable.TABLE + " ADD COLUMN " + ChangelogTable.Columns.UPLOAD_DATE + " TEXT")
} catch (e: Exception) {
AppLog.e(e)
}
@@ -121,9 +121,9 @@ abstract class AppsDatabase : RoomDatabase() {
}
private val MIGRATION_12_13 = object : Migration(12, 13) {
- override fun migrate(database: SupportSQLiteDatabase) {
+ override fun migrate(db: SupportSQLiteDatabase) {
try {
- database.execSQL("ALTER TABLE " + ChangelogTable.TABLE + " ADD COLUMN " + ChangelogTable.Columns.UPLOAD_DATE + " TEXT")
+ db.execSQL("ALTER TABLE " + ChangelogTable.TABLE + " ADD COLUMN " + ChangelogTable.Columns.UPLOAD_DATE + " TEXT")
} catch (e: Exception) {
AppLog.e(e)
}
@@ -131,21 +131,21 @@ abstract class AppsDatabase : RoomDatabase() {
}
private val MIGRATION_13_14 = object : Migration(13, 14) {
- override fun migrate(database: SupportSQLiteDatabase) {
+ override fun migrate(db: SupportSQLiteDatabase) {
AppLog.e("Migrate db from 13 to 14")
- database.execSQL("UPDATE app_list SET update_date = 0 WHERE update_date IS NULL")
- database.execSQL("UPDATE app_list SET ver_name = '' WHERE ver_name IS NULL")
- database.execSQL("UPDATE app_list SET creator = '' WHERE creator IS NULL")
- database.execSQL("UPDATE app_list SET title = app_id WHERE title IS NULL")
- database.execSQL("UPDATE app_list SET iconUrl = '' WHERE iconUrl IS NULL")
- database.execSQL("UPDATE app_list SET upload_date = '' WHERE upload_date IS NULL")
- database.execSQL("UPDATE app_list SET app_type = '' WHERE app_type IS NULL")
- database.execSQL("UPDATE app_list SET price_text = '' WHERE price_text IS NULL")
- database.execSQL("UPDATE app_list SET price_currency = '' WHERE price_currency IS NULL")
- database.execSQL("UPDATE app_list SET price_micros = 0 WHERE price_micros IS NULL")
- database.execSQL("UPDATE app_list SET sync_version = 0 WHERE sync_version IS NULL")
- database.execSQL("CREATE TABLE IF NOT EXISTS `app_list_temp` " +
+ db.execSQL("UPDATE app_list SET update_date = 0 WHERE update_date IS NULL")
+ db.execSQL("UPDATE app_list SET ver_name = '' WHERE ver_name IS NULL")
+ db.execSQL("UPDATE app_list SET creator = '' WHERE creator IS NULL")
+ db.execSQL("UPDATE app_list SET title = app_id WHERE title IS NULL")
+ db.execSQL("UPDATE app_list SET iconUrl = '' WHERE iconUrl IS NULL")
+ db.execSQL("UPDATE app_list SET upload_date = '' WHERE upload_date IS NULL")
+ db.execSQL("UPDATE app_list SET app_type = '' WHERE app_type IS NULL")
+ db.execSQL("UPDATE app_list SET price_text = '' WHERE price_text IS NULL")
+ db.execSQL("UPDATE app_list SET price_currency = '' WHERE price_currency IS NULL")
+ db.execSQL("UPDATE app_list SET price_micros = 0 WHERE price_micros IS NULL")
+ db.execSQL("UPDATE app_list SET sync_version = 0 WHERE sync_version IS NULL")
+ db.execSQL("CREATE TABLE IF NOT EXISTS `app_list_temp` " +
"(`_id` INTEGER NOT NULL, " +
"`app_id` TEXT NOT NULL, " +
"`package` TEXT NOT NULL, " +
@@ -163,7 +163,7 @@ abstract class AppsDatabase : RoomDatabase() {
"`price_text` TEXT NOT NULL, " +
"`price_currency` TEXT NOT NULL, " +
"`price_micros` INTEGER, PRIMARY KEY(`_id`))")
- database.execSQL("INSERT INTO app_list_temp (" +
+ db.execSQL("INSERT INTO app_list_temp (" +
"_id, app_id, package, ver_num, ver_name, title, creator," +
"iconUrl, status, upload_date, details_url, update_date, app_type," +
"sync_version, price_text, price_currency, price_micros) " +
@@ -172,11 +172,11 @@ abstract class AppsDatabase : RoomDatabase() {
"sync_version, price_text, price_currency, price_micros " +
"FROM app_list"
)
- database.execSQL("DROP TABLE app_list")
- database.execSQL("ALTER TABLE app_list_temp RENAME TO app_list")
+ db.execSQL("DROP TABLE app_list")
+ db.execSQL("ALTER TABLE app_list_temp RENAME TO app_list")
- database.execSQL("UPDATE changelog SET upload_date = '' WHERE upload_date IS NULL")
- database.execSQL(
+ db.execSQL("UPDATE changelog SET upload_date = '' WHERE upload_date IS NULL")
+ db.execSQL(
"CREATE TABLE IF NOT EXISTS `changelog_temp` (" +
"`_id` INTEGER NOT NULL, " +
"`app_id` TEXT NOT NULL, " +
@@ -185,38 +185,38 @@ abstract class AppsDatabase : RoomDatabase() {
"`details` TEXT NOT NULL, " +
"`upload_date` TEXT NOT NULL, PRIMARY KEY(`_id`))"
)
- database.execSQL(
+ db.execSQL(
"INSERT INTO changelog_temp (_id, app_id, code, name, details, upload_date)" +
" SELECT _id, app_id, code, name, details, upload_date FROM changelog"
)
- database.execSQL("DROP TABLE changelog")
- database.execSQL("ALTER TABLE changelog_temp RENAME TO changelog")
- database.execSQL("CREATE UNIQUE INDEX `index_changelog_app_id_code` ON `changelog` (`app_id`, `code`)")
+ db.execSQL("DROP TABLE changelog")
+ db.execSQL("ALTER TABLE changelog_temp RENAME TO changelog")
+ db.execSQL("CREATE UNIQUE INDEX `index_changelog_app_id_code` ON `changelog` (`app_id`, `code`)")
- database.execSQL(
+ db.execSQL(
"CREATE TABLE IF NOT EXISTS `app_tags_temp` (" +
"`_id` INTEGER NOT NULL, " +
"`app_id` TEXT NOT NULL, " +
"`tags_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))"
)
- database.execSQL("INSERT INTO app_tags_temp (_id, app_id, tags_id) SELECT _id, app_id, tags_id FROM app_tags")
- database.execSQL("DROP TABLE app_tags")
- database.execSQL("ALTER TABLE app_tags_temp RENAME TO app_tags")
+ db.execSQL("INSERT INTO app_tags_temp (_id, app_id, tags_id) SELECT _id, app_id, tags_id FROM app_tags")
+ db.execSQL("DROP TABLE app_tags")
+ db.execSQL("ALTER TABLE app_tags_temp RENAME TO app_tags")
- database.execSQL(
+ db.execSQL(
"CREATE TABLE IF NOT EXISTS `tags_temp` (" +
"`_id` INTEGER NOT NULL, " +
"`name` TEXT NOT NULL, " +
"`color` INTEGER NOT NULL, PRIMARY KEY(`_id`))"
)
- database.execSQL("INSERT INTO tags_temp (_id, name, color) SELECT _id, name, color FROM tags")
- database.execSQL("DROP TABLE tags")
- database.execSQL("ALTER TABLE tags_temp RENAME TO tags")
+ db.execSQL("INSERT INTO tags_temp (_id, name, color) SELECT _id, name, color FROM tags")
+ db.execSQL("DROP TABLE tags")
+ db.execSQL("ALTER TABLE tags_temp RENAME TO tags")
}
}
private val MIGRATION_18_19 = object : Migration(18, 19) {
- override fun migrate(database: SupportSQLiteDatabase) {
+ override fun migrate(db: SupportSQLiteDatabase) {
AppLog.d("Migrated db from 18 to 19")
}
}
diff --git a/app/src/main/java/com/anod/appwatcher/database/entities/App.kt b/app/src/main/java/com/anod/appwatcher/database/entities/App.kt
index 68013592..a4dc1116 100644
--- a/app/src/main/java/com/anod/appwatcher/database/entities/App.kt
+++ b/app/src/main/java/com/anod/appwatcher/database/entities/App.kt
@@ -18,6 +18,7 @@ import info.anodsplace.framework.content.InstalledPackageApp
import info.anodsplace.framework.content.getAppTitle
import info.anodsplace.framework.content.getLaunchComponent
import info.anodsplace.framework.content.getPackageInfoOrNull
+import kotlinx.serialization.Serializable
import java.text.DateFormat
import java.util.Date
@@ -34,6 +35,7 @@ fun PackageManager.packageToApp(rowId: Int, packageName: String): App {
}
@Entity(tableName = AppListTable.TABLE)
+@Serializable
data class App(
@PrimaryKey @ColumnInfo(name = BaseColumns._ID) val rowId: Int,
diff --git a/app/src/main/java/com/anod/appwatcher/database/entities/Price.kt b/app/src/main/java/com/anod/appwatcher/database/entities/Price.kt
index 2d8999d4..1470600e 100644
--- a/app/src/main/java/com/anod/appwatcher/database/entities/Price.kt
+++ b/app/src/main/java/com/anod/appwatcher/database/entities/Price.kt
@@ -4,12 +4,14 @@ import android.os.Parcelable
import androidx.room.ColumnInfo
import com.anod.appwatcher.database.AppListTable
import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
/**
* @author Alex Gavrishev
* @date 25/05/2018
*/
@Parcelize
+@Serializable
data class Price(
@ColumnInfo(name = AppListTable.Columns.PRICE_TEXT)
val text: String,
diff --git a/app/src/main/java/com/anod/appwatcher/database/entities/Tag.kt b/app/src/main/java/com/anod/appwatcher/database/entities/Tag.kt
index 6138c12f..fb04f323 100644
--- a/app/src/main/java/com/anod/appwatcher/database/entities/Tag.kt
+++ b/app/src/main/java/com/anod/appwatcher/database/entities/Tag.kt
@@ -10,6 +10,7 @@ import androidx.room.PrimaryKey
import com.anod.appwatcher.database.TagsTable
import info.anodsplace.ktx.hashCodeOf
import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
/**
* @author Alex Gavrishev
@@ -17,6 +18,7 @@ import kotlinx.parcelize.Parcelize
*/
@Entity(tableName = TagsTable.TABLE)
@Parcelize
+@Serializable
data class Tag(
@PrimaryKey
@ColumnInfo(name = BaseColumns._ID)
@@ -24,7 +26,7 @@ data class Tag(
@ColumnInfo(name = TagsTable.Columns.NAME)
val name: String,
@ColumnInfo(name = TagsTable.Columns.COLOR)
- @ColorInt
+ @param:ColorInt
val color: Int
) : Parcelable {
diff --git a/app/src/main/java/com/anod/appwatcher/details/DetailsPanel.kt b/app/src/main/java/com/anod/appwatcher/details/DetailsPanel.kt
index 25a67d40..4b92b660 100644
--- a/app/src/main/java/com/anod/appwatcher/details/DetailsPanel.kt
+++ b/app/src/main/java/com/anod/appwatcher/details/DetailsPanel.kt
@@ -103,7 +103,7 @@ import com.anod.appwatcher.compose.TagIcon
import com.anod.appwatcher.compose.TranslateIcon
import com.anod.appwatcher.compose.UninstallIcon
import com.anod.appwatcher.compose.WatchedIcon
-import com.anod.appwatcher.compose.rememberViwModeStoreOwner
+import com.anod.appwatcher.compose.rememberViewModeStoreOwner
import com.anod.appwatcher.database.entities.App
import com.anod.appwatcher.database.entities.AppChange
import com.anod.appwatcher.database.entities.Price
@@ -113,12 +113,13 @@ import com.anod.appwatcher.utils.StoreIntent
import info.anodsplace.applog.AppLog
import info.anodsplace.compose.placeholder
import info.anodsplace.compose.toAnnotatedString
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.content.showToast
+import info.anodsplace.framework.content.startActivity
import info.anodsplace.framework.text.Html
-import java.text.DateFormat
-import java.util.Date
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
+import java.text.DateFormat
+import java.util.Date
private val iconSizeBig = 64.dp
private val iconSizeSmall = 32.dp
@@ -126,8 +127,8 @@ private val iconSizeSmall = 32.dp
private val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT)
@Composable
-fun DetailsPanel(app: App, onDismissRequest: () -> Unit, onCommonActivityAction: (action: CommonActivityAction) -> Unit) {
- val storeOwner = rememberViwModeStoreOwner()
+fun DetailsPanel(app: App, onDismissRequest: () -> Unit) {
+ val storeOwner = rememberViewModeStoreOwner()
val isSystemInDarkTheme = isSystemInDarkTheme()
val viewModel: DetailsViewModel = viewModel(
key = "details-${app.appId}-${app.rowId}",
@@ -150,14 +151,13 @@ fun DetailsPanel(app: App, onDismissRequest: () -> Unit, onCommonActivityAction:
modifier = Modifier.fillMaxSize(),
viewActions = viewModel.viewActions,
onDismissRequest = onDismissRequest,
- onCommonActivityAction = onCommonActivityAction
)
}
}
@Composable
-fun DetailsDialog(app: App, onDismissRequest: () -> Unit, onCommonActivityAction: (action: CommonActivityAction) -> Unit) {
- val storeOwner = rememberViwModeStoreOwner()
+fun DetailsDialog(app: App, onDismissRequest: () -> Unit) {
+ val storeOwner = rememberViewModeStoreOwner()
val isSystemInDarkTheme = isSystemInDarkTheme()
val viewModel: DetailsViewModel = viewModel(
key = "details-${app.appId}-${app.rowId}",
@@ -179,7 +179,6 @@ fun DetailsDialog(app: App, onDismissRequest: () -> Unit, onCommonActivityAction
screenState = screenState,
viewActions = viewModel.viewActions,
onEvent = viewModel::handleEvent,
- onCommonActivityAction = { onCommonActivityAction(it) },
onDismissRequest = onDismissRequest,
modifier = Modifier.fillMaxHeight(fraction = 0.9f)
)
@@ -219,7 +218,6 @@ private fun DetailsScreenContent(
onEvent: (DetailsEvent) -> Unit,
viewActions: Flow,
onDismissRequest: () -> Unit,
- onCommonActivityAction: (CommonActivityAction) -> Unit,
modifier: Modifier = Modifier,
) {
LaunchedEffect(key1 = onEvent) {
@@ -344,7 +342,7 @@ private fun DetailsScreenContent(
val context = LocalContext.current
var showTagList: App? by remember { mutableStateOf(null) }
- LaunchedEffect(key1 = onCommonActivityAction, key2 = onDismissRequest) {
+ LaunchedEffect(onDismissRequest) {
viewActions.collect { action ->
when (action) {
DetailsAction.Dismiss -> {
@@ -352,8 +350,8 @@ private fun DetailsScreenContent(
}
is DetailsAction.Share -> {
- onCommonActivityAction(
- CommonActivityAction.StartActivity(
+ context.startActivity(
+ DetailsAction.StartActivity(
intent = createAppChooser(
action.app,
action.recentChange,
@@ -362,8 +360,6 @@ private fun DetailsScreenContent(
)
)
}
-
- is DetailsAction.ActivityAction -> onCommonActivityAction(action.action)
is DetailsAction.ShowTagSnackbar -> {
val result =
snackBarHostState.showSnackbar(TagSnackbar.Visuals(action.appInfo, context))
@@ -371,6 +367,9 @@ private fun DetailsScreenContent(
showTagList = action.appInfo
}
}
+
+ is DetailsAction.ShowToast -> context.showToast(action)
+ is DetailsAction.StartActivity -> context.startActivity(action)
}
}
}
@@ -1041,7 +1040,6 @@ private fun DetailsScreenPreview() {
modifier = Modifier,
viewActions = flowOf(),
onDismissRequest = { },
- onCommonActivityAction = { }
)
}
}
diff --git a/app/src/main/java/com/anod/appwatcher/details/DetailsViewModel.kt b/app/src/main/java/com/anod/appwatcher/details/DetailsViewModel.kt
index 575afa57..f51079fd 100644
--- a/app/src/main/java/com/anod/appwatcher/details/DetailsViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/details/DetailsViewModel.kt
@@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.BitmapDrawable
-import android.net.Uri
import android.text.format.Formatter
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
@@ -37,7 +36,7 @@ import finsky.api.DfeApi
import finsky.api.Document
import finsky.api.toDocument
import info.anodsplace.applog.AppLog
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.content.ShowToastActionDefaults
import info.anodsplace.framework.content.InstalledApps
import info.anodsplace.framework.content.forAppInfo
import info.anodsplace.framework.content.forUninstall
@@ -53,6 +52,8 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.net.URLEncoder
import java.util.Locale
+import androidx.core.net.toUri
+import info.anodsplace.framework.content.StartActivityAction
typealias TagMenuItem = Pair
@@ -113,28 +114,18 @@ data class AppVersionInfo(
}
sealed interface DetailsAction {
- class ActivityAction(val action: CommonActivityAction) : DetailsAction
+ class StartActivity(override val intent: Intent) : DetailsAction, StartActivityAction
class ShowTagSnackbar(val appInfo: App) : DetailsAction
object Dismiss : DetailsAction
class Share(val app: App, val recentChange: AppChange) : DetailsAction
+ class ShowToast(@param:StringRes override val resId: Int) : ShowToastActionDefaults(resId), DetailsAction
}
-private fun startActivityAction(intent: Intent, addMultiWindowFlags: Boolean = false): DetailsAction.ActivityAction {
- return DetailsAction.ActivityAction(
- action = CommonActivityAction.StartActivity(
- intent = intent,
- addMultiWindowFlags = addMultiWindowFlags
- )
- )
-}
+private fun startActivityAction(intent: Intent): DetailsAction
+ = DetailsAction.StartActivity(intent = intent)
-private fun showToastAction(@StringRes resId: Int): DetailsAction {
- return DetailsAction.ActivityAction(
- action = CommonActivityAction.ShowToast(
- resId = resId
- )
- )
-}
+private fun showToastAction(@StringRes resId: Int): DetailsAction
+ = DetailsAction.ShowToast(resId = resId)
sealed interface DetailsEvent {
class UpdateTag(val tagId: Int, val checked: Boolean) : DetailsEvent
@@ -280,7 +271,6 @@ class DetailsViewModel(
emitAction(
startActivityAction(
intent = Intent().forAppInfo(viewState.appId),
- addMultiWindowFlags = true
)
)
@@ -291,7 +281,6 @@ class DetailsViewModel(
emitAction(
startActivityAction(
intent = launchIntent,
- addMultiWindowFlags = true
)
)
}
@@ -315,7 +304,6 @@ class DetailsViewModel(
DetailsEvent.PlayStore -> emitAction(
startActivityAction(
intent = Intent().forPlayStore(viewState.appId),
- addMultiWindowFlags = true
)
)
@@ -325,7 +313,7 @@ class DetailsViewModel(
val encoded = URLEncoder.encode(Html.parse(text).toString(), "utf-8")
emitAction(startActivityAction((Intent(Intent.ACTION_VIEW).apply {
data =
- Uri.parse("https://translate.google.com/?sl=auto&tl=$lang&text=$encoded&op=translate")
+ "https://translate.google.com/?sl=auto&tl=$lang&text=$encoded&op=translate".toUri()
})))
}
@@ -334,7 +322,7 @@ class DetailsViewModel(
action = startActivityAction(
Intent(
Intent.ACTION_VIEW,
- Uri.parse(event.url)
+ event.url.toUri()
)
)
)
@@ -378,7 +366,7 @@ class DetailsViewModel(
}
} catch (e: AuthTokenStartIntent) {
loadError = true
- emitAction(startActivityAction(e.intent, addMultiWindowFlags = true))
+ emitAction(startActivityAction(e.intent))
} catch (e: Exception) {
loadError = true
AppLog.e("loadChangelog", e)
diff --git a/app/src/main/java/com/anod/appwatcher/history/HistoryListActivity.kt b/app/src/main/java/com/anod/appwatcher/history/HistoryListActivity.kt
deleted file mode 100644
index f6018f47..00000000
--- a/app/src/main/java/com/anod/appwatcher/history/HistoryListActivity.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.anod.appwatcher.history
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.anod.appwatcher.R
-import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.BaseComposeActivity
-import com.anod.appwatcher.compose.MainDetailScreen
-import com.anod.appwatcher.details.DetailsDialog
-import com.anod.appwatcher.utils.prefs
-import com.anod.appwatcher.watchlist.DetailContent
-import info.anodsplace.framework.content.onCommonActivityAction
-import kotlinx.coroutines.launch
-import org.koin.core.component.KoinComponent
-
-/**
- * @author Alex Gavrishev
- * *
- * @date 16/12/2016.
- */
-class HistoryListActivity : BaseComposeActivity(), KoinComponent {
-
- private val viewModel: HistoryListViewModel by viewModels(factoryProducer = {
- HistoryListViewModel.Factory(
- wideLayout = foldableDevice.layout.value,
- )
- })
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (!viewModel.authenticated) {
- Toast.makeText(this, R.string.choose_an_account, Toast.LENGTH_SHORT).show()
- finish()
- return
- }
-
- setContent {
- AppTheme(
- theme = viewModel.prefs.theme
- ) {
- val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
- if (screenState.wideLayout.isWideLayout) {
- MainDetailScreen(
- wideLayout = screenState.wideLayout,
- main = {
- HistoryListScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent,
- pagingDataFlow = viewModel.pagingData,
- viewActions = viewModel.viewActions,
- onActivityAction = { onCommonActivityAction(it) }
- )
- },
- detail = {
- DetailContent(
- app = screenState.selectedApp,
- onDismissRequest = { viewModel.handleEvent(HistoryListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- )
- } else {
- HistoryListScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent,
- pagingDataFlow = viewModel.pagingData,
- viewActions = viewModel.viewActions,
- onActivityAction = { onCommonActivityAction(it) }
- )
- if (screenState.selectedApp != null) {
- DetailsDialog(
- app = screenState.selectedApp!!,
- onDismissRequest = { viewModel.handleEvent(HistoryListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- }
- }
- }
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- foldableDevice.layout.collect {
- viewModel.handleEvent(HistoryListEvent.SetWideLayout(it))
- }
- }
- }
- }
-
- companion object {
- fun intent(context: Context): Intent = Intent(context, HistoryListActivity::class.java)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/history/HistoryListScreen.kt b/app/src/main/java/com/anod/appwatcher/history/HistoryListScreen.kt
index a336845b..65438ae9 100644
--- a/app/src/main/java/com/anod/appwatcher/history/HistoryListScreen.kt
+++ b/app/src/main/java/com/anod/appwatcher/history/HistoryListScreen.kt
@@ -20,6 +20,7 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -30,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
@@ -38,23 +40,42 @@ import androidx.paging.compose.itemKey
import com.anod.appwatcher.R
import com.anod.appwatcher.compose.SearchTopBar
import com.anod.appwatcher.database.entities.App
+import com.anod.appwatcher.navigation.SceneNavKey
import com.anod.appwatcher.search.ListItem
import com.anod.appwatcher.search.MarketAppItem
import com.anod.appwatcher.search.RetryButton
import com.anod.appwatcher.tags.TagSelectionDialog
import com.anod.appwatcher.tags.TagSnackbar
import com.anod.appwatcher.utils.AppIconLoader
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.app.FoldableDeviceLayout
import kotlinx.coroutines.flow.Flow
import org.koin.java.KoinJavaComponent
+@Composable
+fun HistoryListScreenScene(wideLayout: FoldableDeviceLayout, navigateBack: () -> Unit) {
+ val viewModel: HistoryListViewModel = viewModel(
+ factory = HistoryListViewModel.Factory(
+ wideLayout = wideLayout,
+ ),
+ key = SceneNavKey.Main.toString()
+ )
+ val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
+ HistoryListScreen(
+ screenState = screenState,
+ onEvent = viewModel::handleEvent,
+ pagingDataFlow = viewModel.pagingData,
+ viewActions = viewModel.viewActions,
+ navigateBack = navigateBack
+ )
+}
+
@Composable
fun HistoryListScreen(
screenState: HistoryListState,
pagingDataFlow: Flow>,
onEvent: (HistoryListEvent) -> Unit,
viewActions: Flow,
- onActivityAction: (CommonActivityAction) -> Unit,
+ navigateBack: () -> Unit = {},
appIconLoader: AppIconLoader = KoinJavaComponent.getKoin().get(),
) {
val context = LocalContext.current
@@ -114,7 +135,7 @@ fun HistoryListScreen(
}
var showTagList: App? by remember { mutableStateOf(null) }
- LaunchedEffect(key1 = viewActions, key2 = onActivityAction) {
+ LaunchedEffect(key1 = viewActions) {
viewActions.collect { action ->
when (action) {
is HistoryListAction.ShowTagSnackbar -> {
@@ -123,7 +144,8 @@ fun HistoryListScreen(
showTagList = action.info
}
}
- is HistoryListAction.ActivityAction -> onActivityAction(action.action)
+
+ HistoryListAction.OnBackPress -> navigateBack()
}
}
}
diff --git a/app/src/main/java/com/anod/appwatcher/history/HistoryListViewModel.kt b/app/src/main/java/com/anod/appwatcher/history/HistoryListViewModel.kt
index 95a38356..4094758d 100644
--- a/app/src/main/java/com/anod/appwatcher/history/HistoryListViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/history/HistoryListViewModel.kt
@@ -12,17 +12,19 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.filter
+import com.anod.appwatcher.accounts.AuthTokenBlocking
+import com.anod.appwatcher.accounts.toAndroidAccount
import com.anod.appwatcher.database.AppsDatabase
import com.anod.appwatcher.database.entities.App
import com.anod.appwatcher.database.observePackages
import com.anod.appwatcher.search.ListItem
import com.anod.appwatcher.search.updateRowId
import com.anod.appwatcher.utils.BaseFlowViewModel
+import com.anod.appwatcher.utils.prefs
import finsky.api.DfeApi
import finsky.api.FilterComposite
import finsky.api.FilterPredicate
import info.anodsplace.framework.app.FoldableDeviceLayout
-import info.anodsplace.framework.content.CommonActivityAction
import info.anodsplace.framework.content.InstalledApps
import info.anodsplace.playstore.AppNameFilter
import info.anodsplace.playstore.PaidHistoryFilter
@@ -30,6 +32,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -43,8 +46,8 @@ data class HistoryListState(
)
sealed interface HistoryListAction {
+ data object OnBackPress : HistoryListAction
class ShowTagSnackbar(val info: App) : HistoryListAction
- class ActivityAction(val action: CommonActivityAction) : HistoryListAction
}
sealed interface HistoryListEvent {
@@ -69,12 +72,11 @@ class HistoryListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel
private val dfeApi: DfeApi by inject()
private val packageManager: PackageManager by inject()
private val installedApps by lazy { InstalledApps.MemoryCache(InstalledApps.PackageManager(packageManager)) }
- val authenticated: Boolean
- get() = dfeApi.authenticated
+ private val authToken: AuthTokenBlocking by inject()
init {
viewState = HistoryListState(
- wideLayout = wideLayout
+ wideLayout = wideLayout,
)
}
@@ -102,6 +104,7 @@ class HistoryListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel
)
}
.flow
+ .onStart { authToken.checkToken(prefs.account?.toAndroidAccount()) }
.cachedIn(viewModelScope)
.combine(
flow = viewStates.map { it.nameFilter }.distinctUntilChanged(),
@@ -114,7 +117,7 @@ class HistoryListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel
override fun handleEvent(event: HistoryListEvent) {
when (event) {
- HistoryListEvent.OnBackPress -> emitAction(HistoryListAction.ActivityAction(CommonActivityAction.Finish))
+ HistoryListEvent.OnBackPress -> emitAction(HistoryListAction.OnBackPress)
is HistoryListEvent.OnNameFilter -> viewState = viewState.copy(nameFilter = event.query)
is HistoryListEvent.SelectApp -> {
viewState = viewState.copy(selectedApp = event.app)
diff --git a/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt b/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt
deleted file mode 100644
index 29ebaecd..00000000
--- a/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (c) 2020. Alex Gavrishev
-package com.anod.appwatcher.installed
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.annotation.Keep
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.BaseComposeActivity
-import com.anod.appwatcher.compose.MainDetailScreen
-import com.anod.appwatcher.details.DetailsDialog
-import com.anod.appwatcher.model.Filters
-import com.anod.appwatcher.preferences.Preferences
-import com.anod.appwatcher.utils.prefs
-import com.anod.appwatcher.watchlist.DetailContent
-import com.anod.appwatcher.watchlist.MainActivity
-import com.anod.appwatcher.watchlist.WatchListPagingSource
-import info.anodsplace.framework.content.onCommonActivityAction
-import kotlinx.coroutines.launch
-
-@Keep
-class InstalledActivity : BaseComposeActivity() {
- private val viewModel: InstalledListViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- viewModel.handleEvent(InstalledListEvent.SetWideLayout(foldableDevice.layout.value))
-
- setContent {
- AppTheme(
- theme = viewModel.prefs.theme
- ) {
- val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
-
- val pagingSourceConfig = WatchListPagingSource.Config(
- filterId = Filters.ALL,
- tagId = null,
- showRecentlyDiscovered = false,
- showOnDevice = true,
- showRecentlyInstalled = false
- )
-
- if (screenState.wideLayout.isWideLayout) {
- MainDetailScreen(
- wideLayout = screenState.wideLayout,
- main = {
- InstalledListScreen(
- screenState = screenState,
- pagingSourceConfig = pagingSourceConfig,
- onEvent = viewModel::handleEvent,
- installedApps = viewModel.installedApps
- )
- },
- detail = {
- DetailContent(
- app = screenState.selectedApp,
- onDismissRequest = { viewModel.handleEvent(InstalledListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- )
- } else {
- InstalledListScreen(
- screenState = screenState,
- pagingSourceConfig = pagingSourceConfig,
- onEvent = viewModel::handleEvent,
- installedApps = viewModel.installedApps
- )
- if (screenState.selectedApp != null) {
- DetailsDialog(
- app = screenState.selectedApp!!,
- onDismissRequest = { viewModel.handleEvent(InstalledListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- }
- }
- }
-
- lifecycleScope.launch {
- viewModel.viewActions.collect { onCommonActivityAction(it) }
- }
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- foldableDevice.layout.collect {
- viewModel.handleEvent(InstalledListEvent.SetWideLayout(it))
- }
- }
- }
- }
-
- companion object {
- private fun intent(sortId: Int, showImportAction: Boolean, context: Context): Intent {
- return Intent(context, InstalledActivity::class.java).apply {
- putExtra(MainActivity.ARG_SORT, sortId)
- putExtra(MainActivity.ARG_SHOW_ACTION, showImportAction)
- }
- }
-
- fun intent(importMode: Boolean, context: Context) = intent(
- if (importMode) Preferences.SORT_NAME_ASC else Preferences.SORT_DATE_DESC,
- importMode,
- context
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/installed/InstalledListScreen.kt b/app/src/main/java/com/anod/appwatcher/installed/InstalledListScreen.kt
index d598d2b5..9a1b4257 100644
--- a/app/src/main/java/com/anod/appwatcher/installed/InstalledListScreen.kt
+++ b/app/src/main/java/com/anod/appwatcher/installed/InstalledListScreen.kt
@@ -17,14 +17,57 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.anod.appwatcher.R
+import com.anod.appwatcher.model.Filters
+import com.anod.appwatcher.navigation.SceneNavKey
+import com.anod.appwatcher.preferences.Preferences
import com.anod.appwatcher.watchlist.WatchListPage
import com.anod.appwatcher.watchlist.WatchListPagingSource
import info.anodsplace.applog.AppLog
import info.anodsplace.framework.content.InstalledApps
+import info.anodsplace.framework.content.onScreenCommonAction
+
+@Composable
+fun InstalledListScreenScene(
+ showAction: Boolean,
+ navigateBack: () -> Unit
+) {
+ val context = LocalContext.current
+ val viewModel: InstalledListViewModel = viewModel(
+ factory = InstalledListViewModel.Factory(
+ showAction = showAction,
+ sortId = if (showAction) Preferences.SORT_NAME_ASC else Preferences.SORT_DATE_DESC
+ ),
+ key = SceneNavKey.Installed.toString()
+ )
+ val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
+
+ val pagingSourceConfig = WatchListPagingSource.Config(
+ filterId = Filters.ALL,
+ tagId = null,
+ showRecentlyDiscovered = false,
+ showOnDevice = true,
+ showRecentlyInstalled = false
+ )
+
+ InstalledListScreen(
+ screenState = screenState,
+ pagingSourceConfig = pagingSourceConfig,
+ onEvent = viewModel::handleEvent,
+ installedApps = viewModel.installedApps
+ )
+
+ LaunchedEffect(true) {
+ viewModel.viewActions.collect { action ->
+ context.onScreenCommonAction(action, navigateBack)
+ }
+ }
+}
@Composable
fun InstalledListScreen(
diff --git a/app/src/main/java/com/anod/appwatcher/installed/InstalledListViewModel.kt b/app/src/main/java/com/anod/appwatcher/installed/InstalledListViewModel.kt
index 8f4b3154..31eea1cd 100644
--- a/app/src/main/java/com/anod/appwatcher/installed/InstalledListViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/installed/InstalledListViewModel.kt
@@ -5,7 +5,11 @@ import android.graphics.Rect
import android.widget.Toast
import androidx.compose.runtime.Immutable
import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.CreationExtras
import com.anod.appwatcher.R
import com.anod.appwatcher.accounts.AuthTokenBlocking
import com.anod.appwatcher.accounts.CheckTokenError
@@ -21,14 +25,15 @@ import com.anod.appwatcher.utils.networkConnection
import com.anod.appwatcher.utils.prefs
import com.anod.appwatcher.watchlist.WatchListEvent
import info.anodsplace.framework.app.FoldableDeviceLayout
-import info.anodsplace.framework.content.CommonActivityAction
import info.anodsplace.framework.content.InstalledApps
+import info.anodsplace.framework.content.ScreenCommonAction
import info.anodsplace.framework.content.getInstalledPackagesCodes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
+import kotlin.reflect.KClass
@Immutable
data class InstalledListState(
@@ -55,10 +60,13 @@ sealed interface InstalledListEvent {
class SelectApp(val app: App?) : InstalledListEvent
class AuthTokenError(val error: CheckTokenError) : InstalledListEvent
object Import : InstalledListEvent
- object NoAccount : InstalledListEvent
}
-class InstalledListViewModel(state: SavedStateHandle) : BaseFlowViewModel(), KoinComponent {
+class InstalledListViewModel(
+ state: SavedStateHandle,
+ showAction: Boolean,
+ sortId: Int
+) : BaseFlowViewModel(), KoinComponent {
private val importManager: ImportBulkManager by inject()
private val packageManager: PackageManager by inject()
private val packageChanged: PackageChangedReceiver by inject()
@@ -66,10 +74,24 @@ class InstalledListViewModel(state: SavedStateHandle) : BaseFlowViewModel create(modelClass: KClass, extras: CreationExtras): T {
+ return InstalledListViewModel(
+ state = extras.createSavedStateHandle(),
+ showAction = showAction,
+ sortId = sortId,
+ ) as T
+ }
+ }
+
init {
viewState = InstalledListState(
- sortId = state.getInt("sort"),
- selectionMode = state["showAction"] ?: false,
+ sortId = state.getInt("sort", sortId) ,
+ selectionMode = state["showAction"] ?: showAction,
enablePullToRefresh = prefs.enablePullToRefresh
)
viewModelScope.launch {
@@ -108,27 +130,25 @@ class InstalledListViewModel(state: SavedStateHandle) : BaseFlowViewModel import()
is InstalledListEvent.AuthTokenError -> {
if (event.error is CheckTokenError.RequiresInteraction) {
- emitAction(CommonActivityAction.StartActivity(event.error.intent))
+ emitAction(ScreenCommonAction.StartActivity(event.error.intent))
} else {
tokenErrorToast()
}
}
-
- InstalledListEvent.NoAccount -> tokenErrorToast()
}
}
private fun tokenErrorToast() {
if (networkConnection.isNetworkAvailable) {
emitAction(
- CommonActivityAction.ShowToast(
+ ScreenCommonAction.ShowToast(
resId = R.string.failed_gain_access,
length = Toast.LENGTH_SHORT
)
)
} else {
emitAction(
- CommonActivityAction.ShowToast(
+ ScreenCommonAction.ShowToast(
resId = R.string.check_connection,
length = Toast.LENGTH_SHORT
)
@@ -141,10 +161,10 @@ class InstalledListViewModel(state: SavedStateHandle) : BaseFlowViewModel {
- handleEvent(InstalledListEvent.AuthTokenError(result.error))
- false
- }
-
- is CheckTokenResult.Success -> true
+ return when (val result = authToken.checkToken(prefs.account?.toAndroidAccount())) {
+ is CheckTokenResult.Error -> {
+ handleEvent(InstalledListEvent.AuthTokenError(result.error))
+ false
}
+
+ is CheckTokenResult.Success -> true
}
}
diff --git a/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt b/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt
new file mode 100644
index 00000000..d896242a
--- /dev/null
+++ b/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt
@@ -0,0 +1,45 @@
+package com.anod.appwatcher.navigation
+
+import androidx.navigation3.runtime.NavKey
+import com.anod.appwatcher.database.entities.App
+import com.anod.appwatcher.database.entities.Tag
+import kotlinx.serialization.Serializable
+
+sealed interface SceneNavKey : NavKey {
+ @Serializable
+ data object Main : SceneNavKey
+
+ @Serializable
+ data class Search(
+ val keyword: String = "",
+ val focus: Boolean = false,
+ val initiateSearch: Boolean = false,
+ val isPackageSearch: Boolean = false,
+ val isShareSource: Boolean = false
+ ) : SceneNavKey
+
+ @Serializable
+ data object Settings : SceneNavKey
+
+ @Serializable
+ data object RefreshHistory : SceneNavKey
+
+ @Serializable
+ data object UserLog : SceneNavKey
+
+ @Serializable
+ data class Installed(val importMode: Boolean) : SceneNavKey
+
+ @Serializable
+ data object WishList : SceneNavKey
+
+ @Serializable
+ data object PurchaseHistory : SceneNavKey
+
+ @Serializable
+ data class TagWatchList(val tag: Tag) : SceneNavKey
+
+ @Serializable
+ data class AppDetails(val selectedApp: App) : SceneNavKey
+}
+
diff --git a/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt b/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt
deleted file mode 100644
index c48707f8..00000000
--- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt
+++ /dev/null
@@ -1,199 +0,0 @@
-package com.anod.appwatcher.preferences
-
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.viewModels
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.anod.appwatcher.AppWatcherActivity
-import com.anod.appwatcher.backup.ExportBackupTask
-import com.anod.appwatcher.backup.ImportBackupTask
-import com.anod.appwatcher.backup.gdrive.GDriveSignIn
-import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.BaseComposeActivity
-import com.anod.appwatcher.compose.MainDetailScreen
-import com.anod.appwatcher.utils.prefs
-import com.google.android.gms.auth.api.signin.GoogleSignInAccount
-import com.jakewharton.processphoenix.ProcessPhoenix
-import info.anodsplace.applog.AppLog
-import info.anodsplace.framework.content.onCommonActivityAction
-import info.anodsplace.permissions.AppPermission
-import info.anodsplace.permissions.AppPermissions
-import info.anodsplace.permissions.toRequestInput
-import kotlinx.coroutines.launch
-
-@SuppressLint("Registered")
-open class SettingsActivity : BaseComposeActivity(), GDriveSignIn.Listener {
-
- private lateinit var gDriveErrorIntentRequest: ActivityResultLauncher
- private lateinit var notificationPermissionRequest: ActivityResultLauncher
- private val gDriveSignIn: GDriveSignIn by lazy { GDriveSignIn(this, this) }
- private val viewModel: SettingsViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- viewModel.handleEvent(SettingsViewEvent.SetWideLayout(foldableDevice.layout.value))
-
- gDriveErrorIntentRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { }
- notificationPermissionRequest = registerForActivityResult(AppPermissions.Request()) {
- val enabled = it[AppPermission.PostNotification.value] ?: false
- viewModel.handleEvent(SettingsViewEvent.NotificationPermissionResult(enabled))
- if (!enabled) {
- viewModel.handleEvent(SettingsViewEvent.ShowAppSettings)
- }
- }
-
- setContent {
- AppTheme(
- theme = viewModel.prefs.theme
- ) {
- val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
-
- if (screenState.wideLayout.isWideLayout) {
- MainDetailScreen(
- wideLayout = screenState.wideLayout,
- main = {
- SettingsScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent
- )
- },
- detail = {
- Surface {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- imageVector = Icons.Default.Settings,
- contentDescription = null,
- modifier = Modifier.size(128.dp),
- )
- }
- }
- }
- )
- } else {
- SettingsScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent
- )
- }
- }
- }
-
- lifecycleScope.launch {
- lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.viewActions.collect { action ->
- AppLog.d("Action collected $action")
- handleUiAction(action)
- }
- }
- }
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- foldableDevice.layout.collect {
- viewModel.handleEvent(SettingsViewEvent.SetWideLayout(it))
- }
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- viewModel.handleEvent(SettingsViewEvent.CheckNotificationPermission)
- }
-
- private fun handleUiAction(action: SettingsViewAction) {
- when (action) {
- is SettingsViewAction.ExportResult -> onExportResult(action.result)
- is SettingsViewAction.ImportResult -> onImportResult(action.result)
- SettingsViewAction.GDriveSignIn -> gDriveSignIn.signIn()
- SettingsViewAction.GDriveSignOut -> {
- lifecycleScope.launch { gDriveSignIn.signOut() }
- }
- is SettingsViewAction.GDriveErrorIntent -> gDriveErrorIntentRequest.launch(action.intent)
- SettingsViewAction.Recreate -> {
- this@SettingsActivity.setResult(RESULT_OK, Intent().putExtra("recreateWatchlistOnBack", true))
- this@SettingsActivity.recreate()
- recreateWatchlist()
- }
- SettingsViewAction.Rebirth -> {
- ProcessPhoenix.triggerRebirth(applicationContext, Intent(applicationContext, AppWatcherActivity::class.java))
- }
- SettingsViewAction.RequestNotificationPermission -> notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput())
- is SettingsViewAction.ActivityAction -> onCommonActivityAction(action = action.action)
- }
- }
-
- private fun onImportResult(result: Int) {
- when (result) {
- -1 -> {
- AppLog.d("Importing...")
- }
- else -> {
- AppLog.d("Import finished with code: $result")
- ImportBackupTask.showImportFinishToast(this@SettingsActivity, result)
- }
- }
- }
-
- private fun onExportResult(result: Int) {
- when (result) {
- -1 -> {
- AppLog.d("Exporting...")
- }
- else -> {
- AppLog.d("Export finished with code: $result")
- ExportBackupTask.showFinishToast(this@SettingsActivity, result)
- }
- }
- }
-
- @Deprecated("Deprecated in Java")
- override fun onBackPressed() {
- super.onBackPressed()
- if (viewModel.viewState.recreateWatchlistOnBack) {
- this.recreateWatchlist()
- }
- }
-
- @Deprecated("Deprecated in Java")
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- gDriveSignIn.onActivityResult(requestCode, resultCode, data)
- super.onActivityResult(requestCode, resultCode, data)
- }
-
- private fun recreateWatchlist() {
- val i = Intent(this@SettingsActivity, AppWatcherActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
- startActivity(i)
- }
-
- override fun onGDriveLoginSuccess(googleSignInAccount: GoogleSignInAccount) {
- viewModel.handleEvent(SettingsViewEvent.GDriveLoginResult(true, 0))
- }
-
- override fun onGDriveLoginError(errorCode: Int) {
- viewModel.handleEvent(SettingsViewEvent.GDriveLoginResult(false, errorCode))
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/preferences/SettingsScreen.kt b/app/src/main/java/com/anod/appwatcher/preferences/SettingsScreen.kt
index 8ca62f6d..45df8a43 100644
--- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsScreen.kt
+++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsScreen.kt
@@ -1,5 +1,8 @@
package com.anod.appwatcher.preferences
+import android.app.Activity.RESULT_OK
+import android.content.Context
+import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -22,6 +25,9 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -31,18 +37,103 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation3.runtime.NavKey
+import com.anod.appwatcher.AppWatcherActivity
import com.anod.appwatcher.R
import com.anod.appwatcher.backup.DbBackupManager
+import com.anod.appwatcher.backup.ExportBackupTask
+import com.anod.appwatcher.backup.ImportBackupTask
import com.anod.appwatcher.compose.AppTheme
+import com.jakewharton.processphoenix.ProcessPhoenix
import info.anodsplace.applog.AppLog
import info.anodsplace.compose.IconShapeSelector
import info.anodsplace.compose.Preference
import info.anodsplace.compose.PreferenceItem
import info.anodsplace.compose.PreferencesScreen
import info.anodsplace.compose.key
+import info.anodsplace.framework.app.findActivity
import info.anodsplace.framework.content.CreateDocument
+import info.anodsplace.framework.content.showToast
+import info.anodsplace.framework.content.startActivity
+import info.anodsplace.permissions.AppPermission
+import info.anodsplace.permissions.AppPermissions
+import info.anodsplace.permissions.toRequestInput
import org.koin.java.KoinJavaComponent
+@Composable
+fun SettingsScreenScene(navigateBack: () -> Unit, navigateTo: (NavKey) -> Unit) {
+ val viewModel: SettingsViewModel = viewModel()
+ val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
+ val context = LocalContext.current
+ val notificationPermissionRequest = rememberLauncherForActivityResult(AppPermissions.Request()) {
+ val enabled = it[AppPermission.PostNotification.value] ?: false
+ viewModel.handleEvent(SettingsViewEvent.NotificationPermissionResult(enabled))
+ if (!enabled) {
+ viewModel.handleEvent(SettingsViewEvent.ShowAppSettings)
+ }
+ }
+ val gDriveErrorIntentRequest = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ viewModel.handleEvent(SettingsViewEvent.GDriveActivityResult(it))
+ }
+ SettingsScreen(
+ screenState = screenState,
+ onEvent = viewModel::handleEvent
+ )
+ LaunchedEffect(true) {
+ viewModel.viewActions.collect { action ->
+ when (action) {
+ is SettingsViewAction.ExportResult -> onExportResult(action.result, context)
+ is SettingsViewAction.ImportResult -> onImportResult(action.result, context)
+ is SettingsViewAction.GDriveErrorIntent -> gDriveErrorIntentRequest.launch(action.intent)
+ SettingsViewAction.Recreate -> {
+ context.findActivity().setResult(RESULT_OK, Intent().putExtra("recreateWatchlistOnBack", true))
+ context.findActivity().recreate()
+ recreateWatchlist(context)
+ }
+ SettingsViewAction.Rebirth -> {
+ ProcessPhoenix.triggerRebirth(context.applicationContext, Intent(context.applicationContext, AppWatcherActivity::class.java))
+ }
+ SettingsViewAction.RequestNotificationPermission -> notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput())
+ SettingsViewAction.NavigateBack -> navigateBack()
+ is SettingsViewAction.NavigateTo -> navigateTo(action.navKey)
+ is SettingsViewAction.ShowToast -> context.showToast(action)
+ is SettingsViewAction.StartActivity -> context.startActivity(action)
+ }
+ }
+ }
+}
+
+private fun recreateWatchlist(context: Context) {
+ val i = Intent(context, AppWatcherActivity::class.java)
+ i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+ context.startActivity(i)
+}
+
+private fun onImportResult(result: Int, context: Context) {
+ when (result) {
+ -1 -> {
+ AppLog.d("Importing...")
+ }
+ else -> {
+ AppLog.d("Import finished with code: $result")
+ ImportBackupTask.showImportFinishToast(context, result)
+ }
+ }
+}
+
+private fun onExportResult(result: Int, context: Context) {
+ when (result) {
+ -1 -> {
+ AppLog.d("Exporting...")
+ }
+ else -> {
+ AppLog.d("Export finished with code: $result")
+ ExportBackupTask.showFinishToast(context, result)
+ }
+ }
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(screenState: SettingsViewState, onEvent: (SettingsViewEvent) -> Unit, prefs: Preferences = KoinJavaComponent.getKoin().get()) {
@@ -68,7 +159,7 @@ fun SettingsScreen(screenState: SettingsViewState, onEvent: (SettingsViewEvent)
CenterAlignedTopAppBar(
title = { Text(text = stringResource(id = R.string.navdrawer_item_settings)) },
navigationIcon = {
- IconButton(onClick = { onEvent(SettingsViewEvent.OnBackNav) }) {
+ IconButton(onClick = { onEvent(SettingsViewEvent.NavigateBack) }) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.back))
}
},
diff --git a/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt b/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt
index c9d457da..9ef08545 100644
--- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt
@@ -5,32 +5,34 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
+import androidx.activity.result.ActivityResult
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
+import androidx.navigation3.runtime.NavKey
import androidx.work.Operation
import com.anod.appwatcher.R
import com.anod.appwatcher.backup.ExportBackupTask
import com.anod.appwatcher.backup.ImportBackupTask
+import com.anod.appwatcher.backup.gdrive.GDriveSignIn
import com.anod.appwatcher.backup.gdrive.GDriveSync
import com.anod.appwatcher.backup.gdrive.UploadServiceContentObserver
import com.anod.appwatcher.database.AppsDatabase
import com.anod.appwatcher.database.Cleanup
-import com.anod.appwatcher.sync.SchedulesHistoryActivity
+import com.anod.appwatcher.navigation.SceneNavKey
import com.anod.appwatcher.sync.SyncNotification
import com.anod.appwatcher.sync.SyncScheduler
import com.anod.appwatcher.sync.UpdatedApp
-import com.anod.appwatcher.userLog.UserLogActivity
import com.anod.appwatcher.utils.BaseFlowViewModel
import com.anod.appwatcher.utils.prefs
-import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import info.anodsplace.applog.AppLog
import info.anodsplace.compose.PreferenceItem
import info.anodsplace.context.ApplicationContext
import info.anodsplace.framework.app.FoldableDeviceLayout
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.content.ShowToastActionDefaults
+import info.anodsplace.framework.content.StartActivityAction
import info.anodsplace.framework.content.forAppInfo
import info.anodsplace.notification.NotificationManager
import info.anodsplace.permissions.AppPermission
@@ -60,16 +62,16 @@ sealed interface SettingsViewEvent {
class UpdateIconsShape(val newPath: String) : SettingsViewEvent
class GDriveSyncToggle(val checked: Boolean) : SettingsViewEvent
object GDriveSyncNow : SettingsViewEvent
+ class GDriveActivityResult(val activityResult: ActivityResult) : SettingsViewEvent
class ChangeUpdatePolicy(val frequency: Int, val isWifiOnly: Boolean, val isRequiresCharging: Boolean) : SettingsViewEvent
class UpdateCrashReports(val checked: Boolean) : SettingsViewEvent
class SetRecreateFlag(val item: PreferenceItem, val enabled: Boolean, val update: (Boolean) -> Unit) : SettingsViewEvent
class UpdateTheme(val newTheme: Int) : SettingsViewEvent
- object OnBackNav : SettingsViewEvent
+ object NavigateBack : SettingsViewEvent
object TestNotification : SettingsViewEvent
object OssLicenses : SettingsViewEvent
object OpenUserLog : SettingsViewEvent
object OpenRefreshHistory : SettingsViewEvent
- class GDriveLoginResult(val isSuccess: Boolean, val errorCode: Int) : SettingsViewEvent
object NotificationPermissionRequest : SettingsViewEvent
class NotificationPermissionResult(val granted: Boolean) : SettingsViewEvent
class SetWideLayout(val wideLayout: FoldableDeviceLayout) : SettingsViewEvent
@@ -79,35 +81,23 @@ sealed interface SettingsViewEvent {
}
sealed interface SettingsViewAction {
- class ActivityAction(val action: CommonActivityAction) : SettingsViewAction
- object GDriveSignIn : SettingsViewAction
- object GDriveSignOut : SettingsViewAction
+ data object NavigateBack : SettingsViewAction
+ data class StartActivity(override val intent: Intent) : SettingsViewAction, StartActivityAction
+ class ShowToast(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT) : ShowToastActionDefaults(resId, text, length), SettingsViewAction
class GDriveErrorIntent(val intent: Intent) : SettingsViewAction
object Recreate : SettingsViewAction
object Rebirth : SettingsViewAction
object RequestNotificationPermission : SettingsViewAction
class ExportResult(val result: Int) : SettingsViewAction
class ImportResult(val result: Int) : SettingsViewAction
+ data class NavigateTo(val navKey: NavKey) : SettingsViewAction
}
-private val finishAction = SettingsViewAction.ActivityAction(CommonActivityAction.Finish)
-
-private fun startActivityAction(intent: Intent, addMultiWindowFlags: Boolean = false): SettingsViewAction.ActivityAction {
- return SettingsViewAction.ActivityAction(
- action = CommonActivityAction.StartActivity(
- intent = intent,
- addMultiWindowFlags = addMultiWindowFlags
- )
- )
-}
-
-private fun showToastAction(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): SettingsViewAction.ActivityAction {
- return SettingsViewAction.ActivityAction(
- action = CommonActivityAction.ShowToast(
- resId = resId,
- text = text,
- length = length
- )
+private fun showToastAction(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): SettingsViewAction {
+ return SettingsViewAction.ShowToast(
+ resId = resId,
+ text = text,
+ length = length
)
}
@@ -117,6 +107,7 @@ class SettingsViewModel : BaseFlowViewModel onGDriveLoginResult(event.isSuccess, event.errorCode)
SettingsViewEvent.GDriveSyncNow -> gDriveSyncNow()
is SettingsViewEvent.GDriveSyncToggle -> gDriveSyncToggle(event.checked)
- SettingsViewEvent.OnBackNav -> emitAction(finishAction)
- SettingsViewEvent.OpenRefreshHistory -> emitAction(startActivityAction(
- Intent(application, SchedulesHistoryActivity::class.java),
- addMultiWindowFlags = true
- ))
- SettingsViewEvent.OpenUserLog -> emitAction(startActivityAction(
- Intent(application, UserLogActivity::class.java),
- addMultiWindowFlags = true
+ SettingsViewEvent.NavigateBack -> emitAction(SettingsViewAction.NavigateBack)
+ SettingsViewEvent.OpenRefreshHistory -> emitAction(
+ SettingsViewAction.NavigateTo(
+ navKey = SceneNavKey.RefreshHistory
+ )
+ )
+
+ SettingsViewEvent.OpenUserLog -> emitAction(
+ SettingsViewAction.NavigateTo(
+ navKey = SceneNavKey.UserLog
))
- SettingsViewEvent.OssLicenses -> emitAction(startActivityAction(
+
+ SettingsViewEvent.OssLicenses -> emitAction(
+ SettingsViewAction.StartActivity(
Intent(application, OssLicensesMenuActivity::class.java),
- addMultiWindowFlags = true
))
is SettingsViewEvent.SetRecreateFlag -> {
val result = setRecreateFlag(event.item, event.enabled)
@@ -179,9 +172,9 @@ class SettingsViewModel : BaseFlowViewModel emitAction(startActivityAction(
+ SettingsViewEvent.ShowAppSettings -> emitAction(
+ SettingsViewAction.StartActivity(
intent = Intent().forAppInfo(application.packageName),
- addMultiWindowFlags = true
))
SettingsViewEvent.CheckNotificationPermission -> {
val areNotificationsEnabled = prefs.areNotificationsEnabled
@@ -202,6 +195,7 @@ class SettingsViewModel : BaseFlowViewModel {
viewState = viewState.copy(wideLayout = event.wideLayout)
}
+ is SettingsViewEvent.GDriveActivityResult -> onGDriveActivityResult(event.activityResult)
}
}
@@ -224,19 +218,46 @@ class SettingsViewModel : BaseFlowViewModel emitAction(SettingsViewAction.GDriveErrorIntent(e.intent))
+ is GDriveSignIn.GoogleSignInFailedException -> onGDriveLoginResult(false, e.resultCode)
+ else -> onGDriveLoginResult(false, 0)
+ }
+ }
+ }
} else {
prefs.isDriveSyncEnabled = false
viewState = viewState.copy(
isProgressVisible = false,
items = preferenceItems(prefs, inProgress = false, playServices, application)
)
- emitAction(SettingsViewAction.GDriveSignOut)
+ viewModelScope.launch {
+ gDriveSignIn.signOut()
+ }
+ }
+ }
+
+ private fun onGDriveActivityResult(activityResult: ActivityResult): Unit {
+ viewModelScope.launch {
+ try {
+ gDriveSignIn.onActivityResult(activityResult.resultCode, activityResult.data)
+ onGDriveLoginResult(true, -1)
+ } catch (e: Throwable) {
+ when (e) {
+ is GDriveSignIn.GoogleSignInFailedException -> onGDriveLoginResult(false, e.resultCode)
+ else -> onGDriveLoginResult(false, 0)
+ }
+ }
}
}
private fun gDriveSyncNow() {
- val googleAccount = GoogleSignIn.getLastSignedInAccount(context)
+ val googleAccount = GDriveSignIn.getLastSignedInAccount(context)
if (googleAccount != null) {
appScope.launch {
try {
diff --git a/app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt
deleted file mode 100644
index 8930be33..00000000
--- a/app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.anod.appwatcher.search
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.lifecycle.lifecycleScope
-import com.anod.appwatcher.accounts.AccountSelectionDialog
-import com.anod.appwatcher.accounts.AccountSelectionResult
-import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.BaseComposeActivity
-import com.anod.appwatcher.compose.MainDetailScreen
-import com.anod.appwatcher.details.DetailsDialog
-import com.anod.appwatcher.watchlist.DetailContent
-import info.anodsplace.framework.app.FoldableDeviceLayout
-import info.anodsplace.framework.content.onCommonActivityAction
-import kotlinx.coroutines.launch
-
-open class SearchComposeActivity : BaseComposeActivity() {
- val viewModel: SearchViewModel by viewModels(factoryProducer = {
- SearchViewModel.Factory(
- initialState = intentToState(intent, foldableDevice.layout.value),
- )
- })
-
- private lateinit var accountSelectionDialog: AccountSelectionDialog
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- accountSelectionDialog = AccountSelectionDialog(this, viewModel.prefs)
-
- setContent {
- AppTheme(
- theme = viewModel.prefs.theme
- ) {
- val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
- if (screenState.wideLayout.isWideLayout) {
- MainDetailScreen(
- wideLayout = screenState.wideLayout,
- main = {
- SearchResultsScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent,
- pagingDataFlow = { viewModel.pagingData },
- viewActions = viewModel.viewActions,
- onActivityAction = { onCommonActivityAction(it) },
- onShowAccountDialog = { accountSelectionDialog.show() }
- )
- },
- detail = {
- DetailContent(
- app = screenState.selectedApp,
- onDismissRequest = { viewModel.handleEvent(SearchViewEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- )
- } else {
- SearchResultsScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent,
- pagingDataFlow = { viewModel.pagingData },
- viewActions = viewModel.viewActions,
- onActivityAction = { onCommonActivityAction(it) },
- onShowAccountDialog = { accountSelectionDialog.show() }
- )
- if (screenState.selectedApp != null) {
- DetailsDialog(
- app = screenState.selectedApp!!,
- onDismissRequest = { viewModel.handleEvent(SearchViewEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- }
- }
- }
-
- lifecycleScope.launch {
- accountSelectionDialog.accountSelected.collect { result ->
- when (result) {
- AccountSelectionResult.Canceled -> viewModel.handleEvent(SearchViewEvent.AccountSelectError(errorMessage = ""))
- is AccountSelectionResult.Error -> viewModel.handleEvent(SearchViewEvent.AccountSelectError(errorMessage = result.errorMessage))
- is AccountSelectionResult.Success -> viewModel.handleEvent(SearchViewEvent.AccountSelected(account = result.account))
- }
- }
- }
-
- lifecycleScope.launch {
- foldableDevice.layout.collect {
- viewModel.handleEvent(SearchViewEvent.SetWideLayout(it))
- }
- }
- }
-
- private fun intentToState(intent: Intent?, wideLayout: FoldableDeviceLayout) = SearchViewState(
- wideLayout = wideLayout,
- searchQuery = intent?.getStringExtra(EXTRA_KEYWORD) ?: "",
- isPackageSearch = intent?.getBooleanExtra(EXTRA_PACKAGE, false) ?: false,
- initiateSearch = intent?.getBooleanExtra(EXTRA_EXACT, false) ?: false,
- isShareSource = intent?.getBooleanExtra(EXTRA_SHARE, false) ?: false,
- hasFocus = intent?.getBooleanExtra(EXTRA_FOCUS, false) ?: false
- )
-
- companion object {
- const val EXTRA_KEYWORD = "keyword"
- const val EXTRA_EXACT = "exact"
- const val EXTRA_SHARE = "share"
- const val EXTRA_FOCUS = "focus"
- const val EXTRA_PACKAGE = "package"
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt b/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt
index 6b3dcf5f..43a3999a 100644
--- a/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt
+++ b/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt
@@ -22,6 +22,7 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -33,6 +34,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
@@ -43,6 +45,7 @@ import com.anod.appwatcher.R
import com.anod.appwatcher.compose.AppTheme
import com.anod.appwatcher.compose.SearchTopBar
import com.anod.appwatcher.database.entities.App
+import com.anod.appwatcher.navigation.SceneNavKey
import com.anod.appwatcher.tags.TagSelectionDialog
import com.anod.appwatcher.tags.TagSnackbar
import com.anod.appwatcher.utils.AppIconLoader
@@ -51,20 +54,44 @@ import finsky.api.Document
import finsky.protos.AppDetails
import finsky.protos.DocDetails
import finsky.protos.DocV2
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.app.FoldableDeviceLayout
import info.anodsplace.framework.content.InstalledApps
+import info.anodsplace.framework.content.startActivity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.koin.java.KoinJavaComponent
+fun SceneNavKey.Search.toViewState(wideLayout: FoldableDeviceLayout) = SearchViewState(
+ wideLayout = wideLayout,
+ searchQuery = this.keyword,
+ initiateSearch = this.initiateSearch,
+ isPackageSearch = this.isPackageSearch,
+ isShareSource = this.isShareSource,
+ hasFocus = this.focus
+)
+
+@Composable
+fun SearchResultsScreenScene(initialState: SearchViewState, navigateBack: () -> Unit = {}) {
+ val viewModel: SearchViewModel = viewModel(factory = SearchViewModel.Factory(initialState))
+ val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
+ SearchResultsScreen(
+ screenState = screenState,
+ pagingDataFlow = { viewModel.pagingData },
+ onEvent = viewModel::handleEvent,
+ viewActions = viewModel.viewActions,
+ onShowAccountDialog = { /* accountSelectionDialog.show() */ },
+ navigateBack = navigateBack
+ )
+}
+
@Composable
fun SearchResultsScreen(
screenState: SearchViewState,
pagingDataFlow: () -> Flow>,
onEvent: (SearchViewEvent) -> Unit,
viewActions: Flow,
- onActivityAction: (CommonActivityAction) -> Unit = { },
onShowAccountDialog: () -> Unit = { },
+ navigateBack: () -> Unit = {},
appIconLoader: AppIconLoader = KoinJavaComponent.getKoin().get(),
) {
val context = LocalContext.current
@@ -127,7 +154,7 @@ fun SearchResultsScreen(
var showTagList: Pair? by remember { mutableStateOf(null) }
var deleteNoticeDocument: Document? by remember { mutableStateOf(null) }
- LaunchedEffect(key1 = viewActions, key2 = onShowAccountDialog, key3 = onActivityAction) {
+ LaunchedEffect(key1 = true, key2 = onShowAccountDialog) {
viewActions.collect { action ->
when (action) {
SearchViewAction.ShowAccountDialog -> onShowAccountDialog()
@@ -136,36 +163,37 @@ fun SearchResultsScreen(
message = action.message,
duration = action.duration
)
- if (action.finish) {
- onActivityAction(CommonActivityAction.Finish)
+ if (action.exitScreen) {
+ navigateBack()
}
}
is SearchViewAction.ShowTagSnackbar -> {
- val finishActivity = action.isShareSource
+ val exitScreen = action.isShareSource
val result = snackbarHostState.showSnackbar(TagSnackbar.Visuals(action.info, context))
if (result == SnackbarResult.ActionPerformed) {
- showTagList = Pair(action.info, finishActivity)
- } else if (finishActivity) {
- onActivityAction(CommonActivityAction.Finish)
+ showTagList = Pair(action.info, exitScreen)
+ } else if (exitScreen) {
+ navigateBack()
}
}
is SearchViewAction.AlreadyWatchedNotice -> {
deleteNoticeDocument = action.document
}
- is SearchViewAction.ActivityAction -> onActivityAction(action.action)
+ SearchViewAction.NavigateBack -> navigateBack()
+ is SearchViewAction.StartActivity -> context.startActivity(action)
}
}
}
if (showTagList != null) {
- val (appInfo, finishActivity) = showTagList!!
+ val (appInfo, exitScreen) = showTagList!!
TagSelectionDialog(
appId = appInfo.appId,
appTitle = appInfo.title,
onDismissRequest = {
showTagList = null
- if (finishActivity) {
- onActivityAction(CommonActivityAction.Finish)
+ if (exitScreen) {
+ navigateBack()
}
}
)
@@ -262,8 +290,8 @@ private fun LoadingStatePreview() {
screenState = SearchViewState(searchStatus = SearchStatus.Loading),
pagingDataFlow = { flowOf() },
onEvent = { },
+ viewActions = flowOf(),
appIconLoader = appIconLoader,
- viewActions = flowOf()
)
}
}
@@ -280,8 +308,8 @@ private fun EmptyStatePreview() {
screenState = SearchViewState(searchStatus = SearchStatus.NoResults(query = "")),
pagingDataFlow = { flowOf() },
onEvent = { },
+ viewActions = flowOf(),
appIconLoader = appIconLoader,
- viewActions = flowOf()
)
}
}
@@ -298,8 +326,8 @@ private fun RetryStatePreview() {
screenState = SearchViewState(searchStatus = SearchStatus.Error("")),
pagingDataFlow = { flowOf() },
onEvent = { },
+ viewActions = flowOf(),
appIconLoader = appIconLoader,
- viewActions = flowOf()
)
}
}
@@ -340,8 +368,8 @@ private fun SearchSingleResultPreview() {
),
pagingDataFlow = { flowOf() },
onEvent = { },
+ viewActions = flowOf(),
appIconLoader = appIconLoader,
- viewActions = flowOf()
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/search/SearchViewModel.kt b/app/src/main/java/com/anod/appwatcher/search/SearchViewModel.kt
index 883c5b89..bf50055e 100644
--- a/app/src/main/java/com/anod/appwatcher/search/SearchViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/search/SearchViewModel.kt
@@ -34,8 +34,8 @@ import finsky.api.DfeApi
import finsky.api.Document
import finsky.api.toDocument
import info.anodsplace.framework.app.FoldableDeviceLayout
-import info.anodsplace.framework.content.CommonActivityAction
import info.anodsplace.framework.content.InstalledApps
+import info.anodsplace.framework.content.StartActivityAction
import info.anodsplace.playstore.AppDetailsFilter
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@@ -57,19 +57,17 @@ sealed interface SearchStatus {
}
sealed interface SearchViewAction {
- class ActivityAction(val action: CommonActivityAction) : SearchViewAction
- class ShowSnackbar(val message: String, val duration: SnackbarDuration = SnackbarDuration.Short, val finish: Boolean = false) : SearchViewAction
+ class StartActivity(override val intent: Intent) : SearchViewAction, StartActivityAction
+ class ShowSnackbar(val message: String, val duration: SnackbarDuration = SnackbarDuration.Short, val exitScreen: Boolean = false) : SearchViewAction
data object ShowAccountDialog : SearchViewAction
class ShowTagSnackbar(val info: App, val isShareSource: Boolean) : SearchViewAction
class AlreadyWatchedNotice(val document: Document) : SearchViewAction
+ data object NavigateBack: SearchViewAction
}
-private fun startActivityAction(intent: Intent, finish: Boolean = false): SearchViewAction.ActivityAction {
- return SearchViewAction.ActivityAction(
- action = CommonActivityAction.StartActivity(
- intent = intent,
- finish = finish
- )
+private fun startActivityAction(intent: Intent): SearchViewAction {
+ return SearchViewAction.StartActivity(
+ intent = intent,
)
}
@@ -146,26 +144,22 @@ class SearchViewModel(
is SearchViewEvent.OnSearchEnter -> onSearchRequest(event.query)
is SearchViewEvent.AccountSelectError -> onAccountSelectError(event.errorMessage)
is SearchViewEvent.AccountSelected -> onAccountSelected(event.account)
- SearchViewEvent.OnBackPressed -> onBackPressed()
+ SearchViewEvent.OnBackPressed -> emitAction(SearchViewAction.NavigateBack)
is SearchViewEvent.SelectApp -> {
viewState = viewState.copy(selectedApp = event.app)
}
}
}
- private fun onBackPressed() {
- emitAction(SearchViewAction.ActivityAction(CommonActivityAction.Finish))
- }
-
private fun onAccountSelectError(errorMessage: String) {
if (networkConnection.isNetworkAvailable) {
if (errorMessage.isNotBlank()) {
- emitAction(SearchViewAction.ShowSnackbar(message = errorMessage, duration = SnackbarDuration.Short, finish = true))
+ emitAction(SearchViewAction.ShowSnackbar(message = errorMessage, duration = SnackbarDuration.Short, exitScreen = true))
} else {
- emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.failed_gain_access), duration = SnackbarDuration.Long, finish = true))
+ emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.failed_gain_access), duration = SnackbarDuration.Long, exitScreen = true))
}
} else {
- emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.check_connection), duration = SnackbarDuration.Short, finish = true))
+ emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.check_connection), duration = SnackbarDuration.Short, exitScreen = true))
}
}
@@ -180,7 +174,7 @@ class SearchViewModel(
SearchViewAction.ShowSnackbar(
message = context.getString(R.string.check_connection),
duration = SnackbarDuration.Short,
- finish = false
+ exitScreen = false
)
)
} else if (searchStatus is SearchStatus.Error) {
@@ -188,7 +182,7 @@ class SearchViewModel(
SearchViewAction.ShowSnackbar(
message = context.getString(R.string.error_fetching_info),
duration = SnackbarDuration.Short,
- finish = false
+ exitScreen = false
)
)
}
@@ -289,12 +283,12 @@ class SearchViewModel(
try {
accountInitializer.initialize(account)
} catch (e: AuthTokenStartIntent) {
- emitAction(startActivityAction(intent = e.intent, finish = true))
+ emitAction(startActivityAction(intent = e.intent)) // TODO: finish = true
} catch (e: Exception) {
if (networkConnection.isNetworkAvailable) {
- emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.failed_gain_access), duration = SnackbarDuration.Long, finish = true))
+ emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.failed_gain_access), duration = SnackbarDuration.Long, exitScreen = true))
} else {
- emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.check_connection), duration = SnackbarDuration.Short, finish = true))
+ emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.check_connection), duration = SnackbarDuration.Short, exitScreen = true))
}
}
}
diff --git a/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryActivity.kt b/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryScreen.kt
similarity index 82%
rename from app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryActivity.kt
rename to app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryScreen.kt
index c8f0a700..6a27943a 100644
--- a/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryActivity.kt
+++ b/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryScreen.kt
@@ -1,8 +1,6 @@
// Copyright (c) 2020. Alex Gavrishev
package com.anod.appwatcher.sync
-import android.os.Bundle
-import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -17,66 +15,56 @@ import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
import com.anod.appwatcher.R
import com.anod.appwatcher.compose.AppTheme
import com.anod.appwatcher.compose.BackArrowIconButton
-import com.anod.appwatcher.compose.BaseComposeActivity
-import com.anod.appwatcher.database.AppsDatabase
import com.anod.appwatcher.database.entities.Failed
import com.anod.appwatcher.database.entities.New
import com.anod.appwatcher.database.entities.Schedule
import com.anod.appwatcher.database.entities.Skipped
import com.anod.appwatcher.database.entities.Success
import com.anod.appwatcher.utils.isLightColor
-import info.anodsplace.framework.content.CommonActivityAction
-import info.anodsplace.framework.content.onCommonActivityAction
+import info.anodsplace.framework.content.onScreenCommonAction
+import kotlinx.collections.immutable.ImmutableList
import java.text.DateFormat
-import java.text.SimpleDateFormat
import java.util.Date
-import java.util.Locale
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
-import org.koin.core.component.KoinComponent
-import org.koin.core.component.inject
-
-/**
- * @author Alex Gavrishev
- * @date 04/01/2018
- */
-class SchedulesHistoryActivity : BaseComposeActivity(), KoinComponent {
- private val database: AppsDatabase by inject()
- private val dateFormat = SimpleDateFormat("MMM d, HH:mm:ss", Locale.getDefault())
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- val schedules by database.schedules().load().collectAsState(initial = emptyList())
- SchedulesHistoryScreen(
- schedules = schedules.toPersistentList(),
- dateFormat = dateFormat,
- onActivityAction = { onCommonActivityAction(it) }
- )
+@Composable
+fun SchedulesHistoryScreenScene(navigateBack: () -> Unit) {
+ val viewModel: SchedulesHistoryViewModel = viewModel()
+ val viewState by viewModel.viewStates.collectAsState()
+ SchedulesHistoryScreen(
+ schedules = viewState.schedules,
+ dateFormat = viewState.dateFormat,
+ navigateBack = navigateBack
+ )
+ val context = LocalContext.current
+ LaunchedEffect(true) {
+ viewModel.viewActions.collect { action ->
+ context.onScreenCommonAction(action, navigateBack)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun SchedulesHistoryScreen(schedules: ImmutableList, dateFormat: DateFormat, onActivityAction: (CommonActivityAction) -> Unit) {
+fun SchedulesHistoryScreen(schedules: ImmutableList, dateFormat: DateFormat, navigateBack: () -> Unit) {
AppTheme {
Surface {
Column(modifier = Modifier.fillMaxWidth()) {
CenterAlignedTopAppBar(
title = { Text(text = stringResource(id = R.string.refresh_history)) },
- navigationIcon = { BackArrowIconButton(onClick = { onActivityAction(CommonActivityAction.Finish) }) },
+ navigationIcon = { BackArrowIconButton(onClick = { navigateBack() }) },
)
LazyColumn {
items(schedules.size) { index ->
diff --git a/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryViewModel.kt b/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryViewModel.kt
new file mode 100644
index 00000000..5edf42d3
--- /dev/null
+++ b/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryViewModel.kt
@@ -0,0 +1,46 @@
+package com.anod.appwatcher.sync
+
+import androidx.compose.runtime.Immutable
+import androidx.lifecycle.viewModelScope
+import com.anod.appwatcher.database.AppsDatabase
+import com.anod.appwatcher.database.entities.Schedule
+import com.anod.appwatcher.utils.BaseFlowViewModel
+import info.anodsplace.framework.content.ScreenCommonAction
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+@Immutable
+data class SchedulesHistoryState(
+ val schedules: ImmutableList = persistentListOf(),
+ val dateFormat: DateFormat = SimpleDateFormat("MMM d, HH:mm:ss", Locale.getDefault())
+)
+
+sealed interface SchedulesHistoryStateEvent {
+ data object OnBackNav : SchedulesHistoryStateEvent
+}
+
+class SchedulesHistoryViewModel : BaseFlowViewModel(), KoinComponent {
+ private val database: AppsDatabase by inject()
+
+ init {
+ viewState = SchedulesHistoryState()
+ viewModelScope.launch {
+ database.schedules().load().collect { schedules ->
+ viewState = viewState.copy(schedules = schedules.toPersistentList())
+ }
+ }
+ }
+
+ override fun handleEvent(event: SchedulesHistoryStateEvent) {
+ when (event) {
+ SchedulesHistoryStateEvent.OnBackNav -> emitAction(ScreenCommonAction.NavigateBack)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/sync/SyncNotification.kt b/app/src/main/java/com/anod/appwatcher/sync/SyncNotification.kt
index 9f1841be..29003a60 100644
--- a/app/src/main/java/com/anod/appwatcher/sync/SyncNotification.kt
+++ b/app/src/main/java/com/anod/appwatcher/sync/SyncNotification.kt
@@ -6,12 +6,12 @@ import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import androidx.core.app.NotificationCompat
+import androidx.core.net.toUri
import com.anod.appwatcher.AppWatcherActivity
import com.anod.appwatcher.NotificationActivity
import com.anod.appwatcher.R
import com.anod.appwatcher.preferences.Preferences
import com.anod.appwatcher.utils.color.DynamicColors
-import com.anod.appwatcher.watchlist.MainActivity
import info.anodsplace.context.ApplicationContext
import info.anodsplace.framework.text.Html
@@ -92,9 +92,9 @@ class SyncNotification(private val context: ApplicationContext, private val noti
private fun create(updatedApps: List): Notification {
val notificationIntent = Intent(context.actual, AppWatcherActivity::class.java)
notificationIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
- val data = Uri.parse("com.anod.appwatcher://notification")
+ val data = "com.anod.appwatcher://notification".toUri()
notificationIntent.data = data
- notificationIntent.putExtra(MainActivity.EXTRA_FROM_NOTIFICATION, true)
+ notificationIntent.putExtra(AppWatcherActivity.EXTRA_FROM_NOTIFICATION, true)
val contentIntent = PendingIntent.getActivity(context.actual, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
val title = renderTitle(updatedApps)
diff --git a/app/src/main/java/com/anod/appwatcher/tags/AppsTagDialog.kt b/app/src/main/java/com/anod/appwatcher/tags/AppsTagDialog.kt
index 0b100529..7b4b6ff8 100644
--- a/app/src/main/java/com/anod/appwatcher/tags/AppsTagDialog.kt
+++ b/app/src/main/java/com/anod/appwatcher/tags/AppsTagDialog.kt
@@ -31,7 +31,7 @@ import com.anod.appwatcher.R
import com.anod.appwatcher.compose.AppIconImage
import com.anod.appwatcher.compose.AppTheme
import com.anod.appwatcher.compose.SearchTopBar
-import com.anod.appwatcher.compose.rememberViwModeStoreOwner
+import com.anod.appwatcher.compose.rememberViewModeStoreOwner
import com.anod.appwatcher.database.entities.App
import com.anod.appwatcher.database.entities.Price
import com.anod.appwatcher.database.entities.Tag
@@ -42,7 +42,7 @@ import org.koin.java.KoinJavaComponent.getKoin
@Composable
fun AppsTagDialog(tag: Tag, onDismissRequest: () -> Unit) {
- val storeOwner = rememberViwModeStoreOwner()
+ val storeOwner = rememberViewModeStoreOwner()
val viewModel: AppsTagViewModel = viewModel(
viewModelStoreOwner = storeOwner,
factory = AppsTagViewModel.Factory(tag)
diff --git a/app/src/main/java/com/anod/appwatcher/tags/EditTagDialog.kt b/app/src/main/java/com/anod/appwatcher/tags/EditTagDialog.kt
index 660fdd70..53d12ec4 100644
--- a/app/src/main/java/com/anod/appwatcher/tags/EditTagDialog.kt
+++ b/app/src/main/java/com/anod/appwatcher/tags/EditTagDialog.kt
@@ -38,7 +38,7 @@ import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
import com.anod.appwatcher.R
import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.rememberViwModeStoreOwner
+import com.anod.appwatcher.compose.rememberViewModeStoreOwner
import com.anod.appwatcher.database.entities.Tag
import info.anodsplace.compose.BottomSheet
import info.anodsplace.compose.ButtonsPanel
@@ -46,7 +46,7 @@ import info.anodsplace.compose.ColorDialogContent
@Composable
fun EditTagDialog(tag: Tag, onDismissRequest: (tagId: Int) -> Unit) {
- val storeOwner = rememberViwModeStoreOwner()
+ val storeOwner = rememberViewModeStoreOwner()
val viewModel: EditTagViewModel = viewModel(
key = "tag-${tag.id}",
viewModelStoreOwner = storeOwner,
diff --git a/app/src/main/java/com/anod/appwatcher/tags/EditTagViewModel.kt b/app/src/main/java/com/anod/appwatcher/tags/EditTagViewModel.kt
index 35647606..394112d1 100644
--- a/app/src/main/java/com/anod/appwatcher/tags/EditTagViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/tags/EditTagViewModel.kt
@@ -20,7 +20,7 @@ data class EditTagState(
)
sealed interface EditTagEvent {
- class UpdateColor(@ColorInt val color: Int) : EditTagEvent
+ class UpdateColor(@param:ColorInt val color: Int) : EditTagEvent
class SaveAndDismiss(val name: String) : EditTagEvent
object Delete : EditTagEvent
object Dismiss : EditTagEvent
diff --git a/app/src/main/java/com/anod/appwatcher/tags/TagSelectionDialog.kt b/app/src/main/java/com/anod/appwatcher/tags/TagSelectionDialog.kt
index 4e9963e7..82fe94dd 100644
--- a/app/src/main/java/com/anod/appwatcher/tags/TagSelectionDialog.kt
+++ b/app/src/main/java/com/anod/appwatcher/tags/TagSelectionDialog.kt
@@ -9,8 +9,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.icons.filled.Label
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -34,14 +32,14 @@ import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
import com.anod.appwatcher.R
import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.rememberViwModeStoreOwner
+import com.anod.appwatcher.compose.rememberViewModeStoreOwner
import com.anod.appwatcher.database.entities.Tag
import info.anodsplace.compose.CheckBoxItem
import info.anodsplace.compose.CheckBoxLazyList
@Composable
fun TagSelectionDialog(appId: String, appTitle: String, onDismissRequest: () -> Unit) {
- val storeOwner = rememberViwModeStoreOwner()
+ val storeOwner = rememberViewModeStoreOwner()
val viewModel: TagsSelectionViewModel = viewModel(
key = appId,
viewModelStoreOwner = storeOwner,
diff --git a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt
deleted file mode 100644
index db57e9fa..00000000
--- a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-package com.anod.appwatcher.tags
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.graphics.Color
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.BaseComposeActivity
-import com.anod.appwatcher.compose.MainDetailScreen
-import com.anod.appwatcher.database.entities.Tag
-import com.anod.appwatcher.details.DetailsDialog
-import com.anod.appwatcher.model.Filters
-import com.anod.appwatcher.utils.prefs
-import com.anod.appwatcher.watchlist.DetailContent
-import com.anod.appwatcher.watchlist.WatchListEvent
-import com.anod.appwatcher.watchlist.WatchListPagingSource
-import com.anod.appwatcher.watchlist.WatchListStateViewModel
-import info.anodsplace.framework.app.addMultiWindowFlags
-import info.anodsplace.framework.content.onCommonActivityAction
-import kotlinx.coroutines.launch
-
-class TagWatchListComposeActivity : BaseComposeActivity() {
- private val viewModel: WatchListStateViewModel by viewModels(factoryProducer = {
- WatchListStateViewModel.Factory(
- defaultFilterId = Filters.ALL,
- wideLayout = foldableDevice.layout.value,
- collectRecentlyInstalledApps = false
- )
- })
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
- val customPrimaryColor by remember(screenState) {
- derivedStateOf { Color(screenState.tag.color) }
- }
- AppTheme(
- customPrimaryColor = customPrimaryColor,
- theme = viewModel.prefs.theme
- ) {
- val pagingSourceConfig = WatchListPagingSource.Config(
- filterId = screenState.filterId,
- tagId = screenState.tag.id,
- showRecentlyDiscovered = viewModel.prefs.showRecentlyDiscovered,
- showOnDevice = false,
- showRecentlyInstalled = false
- )
-
- if (screenState.wideLayout.isWideLayout) {
- MainDetailScreen(
- wideLayout = screenState.wideLayout,
- main = {
- TagWatchListScreen(
- screenState = screenState,
- pagingSourceConfig = pagingSourceConfig,
- onEvent = viewModel::handleEvent,
- installedApps = viewModel.installedApps
- )
- },
- detail = {
- DetailContent(
- app = screenState.selectedApp,
- onDismissRequest = { viewModel.handleEvent(WatchListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- )
- } else {
- TagWatchListScreen(
- screenState = screenState,
- pagingSourceConfig = pagingSourceConfig,
- onEvent = viewModel::handleEvent,
- installedApps = viewModel.installedApps
- )
- if (screenState.selectedApp != null) {
- DetailsDialog(
- app = screenState.selectedApp!!,
- onDismissRequest = { viewModel.handleEvent(WatchListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- }
-
- if (screenState.showAppTagDialog) {
- AppsTagDialog(
- tag = screenState.tag,
- onDismissRequest = { viewModel.handleEvent(WatchListEvent.AddAppToTag(show = false)) }
- )
- }
-
- if (screenState.showEditTagDialog) {
- EditTagDialog(
- tag = screenState.tag,
- onDismissRequest = { viewModel.handleEvent(WatchListEvent.EditTag(show = false)) }
- )
- }
- }
- }
-
- lifecycleScope.launch {
- viewModel.viewActions.collect { onCommonActivityAction(it) }
- }
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- foldableDevice.layout.collect {
- viewModel.handleEvent(WatchListEvent.SetWideLayout(it))
- }
- }
- }
- }
-
- @Deprecated("Deprecated in Java")
- override fun onBackPressed() {
- if (viewModel.viewState.wideLayout.isWideLayout) {
- if (viewModel.viewState.selectedApp != null) {
- viewModel.handleEvent(WatchListEvent.SelectApp(app = null))
- } else {
- super.onBackPressed()
- }
- } else {
- super.onBackPressed()
- }
- }
-
- companion object {
- fun createTagIntent(tag: Tag, context: Context) = Intent(context, TagWatchListComposeActivity::class.java).apply {
- putExtra(WatchListStateViewModel.EXTRA_TAG, tag)
- addMultiWindowFlags(context)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListScreen.kt b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListScreen.kt
index 09626d75..458d6971 100644
--- a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListScreen.kt
+++ b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListScreen.kt
@@ -4,19 +4,101 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation3.runtime.NavKey
import com.anod.appwatcher.R
+import com.anod.appwatcher.compose.AppTheme
import com.anod.appwatcher.compose.EditIcon
import com.anod.appwatcher.compose.FilterMenuItem
import com.anod.appwatcher.compose.PinShortcutIcon
import com.anod.appwatcher.compose.SortMenuItem
import com.anod.appwatcher.compose.TagAppIconButton
+import com.anod.appwatcher.database.entities.Tag
+import com.anod.appwatcher.model.Filters
+import com.anod.appwatcher.navigation.SceneNavKey
+import com.anod.appwatcher.utils.prefs
+import com.anod.appwatcher.watchlist.WatchListAction
import com.anod.appwatcher.watchlist.WatchListEvent
import com.anod.appwatcher.watchlist.WatchListPagingSource
import com.anod.appwatcher.watchlist.WatchListScreen
import com.anod.appwatcher.watchlist.WatchListSharedState
+import com.anod.appwatcher.watchlist.WatchListStateViewModel
import com.anod.appwatcher.watchlist.WatchListTopBar
+import info.anodsplace.framework.app.FoldableDeviceLayout
import info.anodsplace.framework.content.InstalledApps
+import info.anodsplace.framework.content.showToast
+import info.anodsplace.framework.content.startActivity
+
+@Composable
+fun TagWatchListScreenScene(wideLayout: FoldableDeviceLayout, tag: Tag, navigateBack: () -> Unit, navigateTo: (NavKey) -> Unit) {
+ val viewModel: WatchListStateViewModel = viewModel(factory =
+ WatchListStateViewModel.Factory(
+ defaultFilterId = Filters.ALL,
+ wideLayout = wideLayout,
+ collectRecentlyInstalledApps = false,
+ initialTag = tag
+ ),
+ key = SceneNavKey.TagWatchList.toString()
+ )
+ val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
+ val context = LocalContext.current
+ val customPrimaryColor by remember(screenState) {
+ derivedStateOf { Color(screenState.tag.color) }
+ }
+ AppTheme(
+ customPrimaryColor = customPrimaryColor,
+ theme = viewModel.prefs.theme
+ ) {
+ val pagingSourceConfig = WatchListPagingSource.Config(
+ filterId = screenState.filterId,
+ tagId = screenState.tag.id,
+ showRecentlyDiscovered = viewModel.prefs.showRecentlyDiscovered,
+ showOnDevice = false,
+ showRecentlyInstalled = false
+ )
+
+ TagWatchListScreen(
+ screenState = screenState,
+ pagingSourceConfig = pagingSourceConfig,
+ onEvent = viewModel::handleEvent,
+ installedApps = viewModel.installedApps
+ )
+
+ if (screenState.showAppTagDialog) {
+ AppsTagDialog(
+ tag = screenState.tag,
+ onDismissRequest = { viewModel.handleEvent(WatchListEvent.AddAppToTag(show = false)) }
+ )
+ }
+
+ if (screenState.showEditTagDialog) {
+ EditTagDialog(
+ tag = screenState.tag,
+ onDismissRequest = { viewModel.handleEvent(WatchListEvent.EditTag(show = false)) }
+ )
+ }
+ }
+
+ LaunchedEffect(true) {
+ viewModel.viewActions.collect {
+ when (it) {
+ is WatchListAction.SelectApp -> {}
+ is WatchListAction.ShowToast -> context.showToast(it)
+ is WatchListAction.StartActivity -> context.startActivity(it)
+ WatchListAction.NavigateBack -> navigateBack()
+ is WatchListAction.NavigateTo -> navigateTo(it.navKey)
+ }
+ }
+ }
+}
@Composable
fun TagWatchListScreen(
diff --git a/app/src/main/java/com/anod/appwatcher/tags/TagsSelectionViewModel.kt b/app/src/main/java/com/anod/appwatcher/tags/TagsSelectionViewModel.kt
index 265c1e55..a9b90139 100644
--- a/app/src/main/java/com/anod/appwatcher/tags/TagsSelectionViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/tags/TagsSelectionViewModel.kt
@@ -2,7 +2,6 @@ package com.anod.appwatcher.tags
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Label
-import androidx.compose.material.icons.filled.Label
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
@@ -14,7 +13,7 @@ import com.anod.appwatcher.database.AppsDatabase
import com.anod.appwatcher.database.entities.Tag
import com.anod.appwatcher.utils.BaseFlowViewModel
import info.anodsplace.compose.CheckBoxItem
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.content.ScreenCommonAction
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@@ -39,7 +38,7 @@ sealed interface TagSelectionEvent {
}
@OptIn(ExperimentalCoroutinesApi::class)
-class TagsSelectionViewModel(appId: String, appTitle: String) : BaseFlowViewModel(), KoinComponent {
+class TagsSelectionViewModel(appId: String, appTitle: String) : BaseFlowViewModel(), KoinComponent {
class Factory(private val appId: String, private val appTitle: String) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
diff --git a/app/src/main/java/com/anod/appwatcher/userLog/UserLogActivity.kt b/app/src/main/java/com/anod/appwatcher/userLog/UserLogActivity.kt
deleted file mode 100644
index 349b9b7f..00000000
--- a/app/src/main/java/com/anod/appwatcher/userLog/UserLogActivity.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.anod.appwatcher.userLog
-
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.lifecycle.lifecycleScope
-import com.anod.appwatcher.compose.BaseComposeActivity
-import info.anodsplace.framework.content.onCommonActivityAction
-import kotlinx.coroutines.launch
-
-/**
- * @author Alex Gavrishev
- * @date 04/01/2018
- */
-class UserLogActivity : BaseComposeActivity() {
- private val viewModel: UserLogViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
-
- UserLogScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent
- )
- }
-
- lifecycleScope.launch {
- viewModel.viewActions.collect { onCommonActivityAction(it) }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/userLog/UserLogScreen.kt b/app/src/main/java/com/anod/appwatcher/userLog/UserLogScreen.kt
index b09b370f..396786f1 100644
--- a/app/src/main/java/com/anod/appwatcher/userLog/UserLogScreen.kt
+++ b/app/src/main/java/com/anod/appwatcher/userLog/UserLogScreen.kt
@@ -13,6 +13,9 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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
@@ -21,15 +24,34 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
import com.anod.appwatcher.R
import com.anod.appwatcher.compose.AppTheme
import com.anod.appwatcher.compose.BackArrowIconButton
import com.anod.appwatcher.compose.ShareIconButton
import com.anod.appwatcher.preferences.Preferences
+import info.anodsplace.framework.content.onScreenCommonAction
+import info.anodsplace.notification.NotificationManager
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import org.koin.java.KoinJavaComponent
+@Composable
+fun UserLogScreenScene(navigateBack: () -> Unit) {
+ val viewModel: UserLogViewModel = viewModel()
+ val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
+ UserLogScreen(
+ screenState = screenState,
+ onEvent = viewModel::handleEvent
+ )
+ val context = LocalContext.current
+ LaunchedEffect(true) {
+ viewModel.viewActions.collect { action ->
+ context.onScreenCommonAction(action, navigateBack)
+ }
+ }
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UserLogScreen(screenState: UserLogState, onEvent: (UserLogEvent) -> Unit, prefs: Preferences = KoinJavaComponent.getKoin().get()) {
@@ -142,6 +164,6 @@ private fun UserLogScreenPreview() {
""".trimIndent().split("\n").map { UserLogMessage.from(it) }.toPersistentList()
),
onEvent = {},
- prefs = Preferences(context, info.anodsplace.notification.NotificationManager.NoOp(), scope)
+ prefs = Preferences(context, NotificationManager.NoOp(), scope)
)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/userLog/UserLogViewModel.kt b/app/src/main/java/com/anod/appwatcher/userLog/UserLogViewModel.kt
index e939f358..514f73b4 100644
--- a/app/src/main/java/com/anod/appwatcher/userLog/UserLogViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/userLog/UserLogViewModel.kt
@@ -3,7 +3,7 @@ package com.anod.appwatcher.userLog
import android.content.Intent
import androidx.compose.runtime.Immutable
import com.anod.appwatcher.utils.BaseFlowViewModel
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.content.ScreenCommonAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@@ -18,7 +18,7 @@ sealed interface UserLogEvent {
data object Share : UserLogEvent
}
-class UserLogViewModel : BaseFlowViewModel() {
+class UserLogViewModel : BaseFlowViewModel() {
private val userLogger = UserLogger()
init {
@@ -29,8 +29,8 @@ class UserLogViewModel : BaseFlowViewModel emitAction(CommonActivityAction.Finish)
- UserLogEvent.Share -> emitAction(CommonActivityAction.StartActivity(intent = Intent().apply {
+ UserLogEvent.OnBackNav -> emitAction(ScreenCommonAction.NavigateBack)
+ UserLogEvent.Share -> emitAction(ScreenCommonAction.StartActivity(intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TITLE, "AppWatcher Log")
putExtra(Intent.EXTRA_TEXT, userLogger.content)
diff --git a/app/src/main/java/com/anod/appwatcher/utils/AndroidVersions.kt b/app/src/main/java/com/anod/appwatcher/utils/AndroidVersions.kt
index 8d99f065..e00b7d57 100644
--- a/app/src/main/java/com/anod/appwatcher/utils/AndroidVersions.kt
+++ b/app/src/main/java/com/anod/appwatcher/utils/AndroidVersions.kt
@@ -37,5 +37,6 @@ val androidVersions = mapOf(
Build.VERSION_CODES.S_V2 to "12",
Build.VERSION_CODES.TIRAMISU to "13",
Build.VERSION_CODES.UPSIDE_DOWN_CAKE to "14",
- 35 to "15"
+ Build.VERSION_CODES.VANILLA_ICE_CREAM to "15",
+ Build.VERSION_CODES.BAKLAVA to "16"
)
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/utils/StoreIntent.kt b/app/src/main/java/com/anod/appwatcher/utils/StoreIntent.kt
index 034be7d4..288ef722 100644
--- a/app/src/main/java/com/anod/appwatcher/utils/StoreIntent.kt
+++ b/app/src/main/java/com/anod/appwatcher/utils/StoreIntent.kt
@@ -4,6 +4,7 @@ import android.content.ComponentName
import android.content.Intent
import android.net.Uri
import android.os.Build
+import androidx.core.net.toUri
/**
* @author Alex Gavrishev
@@ -17,23 +18,16 @@ object StoreIntent {
fun Intent.forPlayStore(pkg: String): Intent {
val url = String.format(StoreIntent.URL_PLAY_STORE, pkg)
this.action = Intent.ACTION_VIEW
- this.data = Uri.parse(url)
+ this.data = url.toUri()
return this
}
fun Intent.forMyApps(update: Boolean): Intent {
action = "com.google.android.finsky.VIEW_MY_DOWNLOADS"
- component = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- ComponentName(
+ component = ComponentName(
"com.android.vending",
"com.android.vending.AssetBrowserActivity"
)
- } else {
- ComponentName(
- "com.android.vending",
- "com.google.android.finsky.activities.MainActivity"
- )
- }
if (update) {
this.putExtra("trigger_update_all", true)
}
diff --git a/app/src/main/java/com/anod/appwatcher/utils/color/ColorRoles.kt b/app/src/main/java/com/anod/appwatcher/utils/color/ColorRoles.kt
index 82108780..a0b80df6 100644
--- a/app/src/main/java/com/anod/appwatcher/utils/color/ColorRoles.kt
+++ b/app/src/main/java/com/anod/appwatcher/utils/color/ColorRoles.kt
@@ -24,17 +24,17 @@ import androidx.annotation.ColorInt
*/
class ColorRoles(
/** Returns the accent color, used as the main color from the color role. */
- @ColorInt val accent: Int,
+ @param:ColorInt val accent: Int,
/**
* Returns the on_accent color, used for content such as icons and text on top of the Accent
* color.
*/
- @ColorInt val onAccent: Int,
+ @param:ColorInt val onAccent: Int,
/** Returns the accent_container color, used with less emphasis than the accent color. */
- @ColorInt val accentContainer: Int,
+ @param:ColorInt val accentContainer: Int,
/**
* Returns the on_accent_container color, used for content such as icons and text on top of the
* accent_container color.
*/
- @ColorInt val onAccentContainer: Int
+ @param:ColorInt val onAccentContainer: Int
)
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/DetailContent.kt b/app/src/main/java/com/anod/appwatcher/watchlist/DetailContent.kt
index 977729fc..ff089ff9 100644
--- a/app/src/main/java/com/anod/appwatcher/watchlist/DetailContent.kt
+++ b/app/src/main/java/com/anod/appwatcher/watchlist/DetailContent.kt
@@ -11,24 +11,27 @@ import androidx.compose.ui.res.painterResource
import com.anod.appwatcher.R
import com.anod.appwatcher.database.entities.App
import com.anod.appwatcher.details.DetailsPanel
-import info.anodsplace.framework.content.CommonActivityAction
@Composable
-fun DetailContent(app: App?, onDismissRequest: () -> Unit, onCommonActivityAction: (action: CommonActivityAction) -> Unit) {
+fun DetailContent(app: App?, onDismissRequest: () -> Unit) {
Surface {
if (app == null) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Image(painter = painterResource(id = R.drawable.ic_empty_box_smile), contentDescription = null)
- }
+ EmptyBoxSmile()
} else {
DetailsPanel(
app = app,
onDismissRequest = onDismissRequest,
- onCommonActivityAction = onCommonActivityAction
)
}
}
+}
+
+@Composable
+fun EmptyBoxSmile() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(painter = painterResource(id = R.drawable.ic_empty_box_smile), contentDescription = null)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/DrawerItem.kt b/app/src/main/java/com/anod/appwatcher/watchlist/DrawerItem.kt
index 0b7e0589..2455fb40 100644
--- a/app/src/main/java/com/anod/appwatcher/watchlist/DrawerItem.kt
+++ b/app/src/main/java/com/anod/appwatcher/watchlist/DrawerItem.kt
@@ -16,7 +16,7 @@ import com.anod.appwatcher.R
data class DrawerItem(
val id: Id,
val icon: ImageVector,
- @StringRes val title: Int
+ @param:StringRes val title: Int
) {
sealed interface Id {
object Refresh : Id
diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt
deleted file mode 100644
index 9eb4969d..00000000
--- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt
+++ /dev/null
@@ -1,239 +0,0 @@
-package com.anod.appwatcher.watchlist
-
-import android.content.Intent
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.viewModels
-import androidx.compose.material3.DrawerValue
-import androidx.compose.material3.rememberDrawerState
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.core.os.bundleOf
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.anod.appwatcher.BuildConfig
-import com.anod.appwatcher.MarketSearchActivity
-import com.anod.appwatcher.R
-import com.anod.appwatcher.SettingsActivity
-import com.anod.appwatcher.accounts.AccountSelectionDialog
-import com.anod.appwatcher.accounts.toAndroidAccount
-import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.BaseComposeActivity
-import com.anod.appwatcher.compose.MainDetailScreen
-import com.anod.appwatcher.database.entities.Tag
-import com.anod.appwatcher.details.DetailsDialog
-import com.anod.appwatcher.history.HistoryListActivity
-import com.anod.appwatcher.installed.InstalledActivity
-import com.anod.appwatcher.tags.TagWatchListComposeActivity
-import com.anod.appwatcher.utils.getIntentFlags
-import com.anod.appwatcher.utils.prefs
-import com.anod.appwatcher.wishlist.WishListActivity
-import info.anodsplace.applog.AppLog
-import info.anodsplace.framework.content.onCommonActivityAction
-import info.anodsplace.permissions.AppPermission
-import info.anodsplace.permissions.AppPermissions
-import info.anodsplace.permissions.toRequestInput
-import kotlinx.coroutines.launch
-import org.koin.core.component.KoinComponent
-
-abstract class MainActivity : BaseComposeActivity(), KoinComponent {
- private val mainViewModel: MainViewModel by viewModels()
- private val listViewModel: WatchListStateViewModel by viewModels(factoryProducer = {
- WatchListStateViewModel.Factory(
- defaultFilterId = prefs.defaultMainFilterId,
- wideLayout = foldableDevice.layout.value,
- collectRecentlyInstalledApps = prefs.showRecent
- )
- })
- private lateinit var accountSelectionDialog: AccountSelectionDialog
- private lateinit var notificationPermissionRequest: ActivityResultLauncher
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val extras = intent?.extras ?: bundleOf()
- if (BuildConfig.DEBUG) {
- AppLog.d("Intent flags: ${getIntentFlags(intent?.flags ?: 0).joinToString()}")
- }
- if (extras.containsKey("open_recently_installed")) {
- intent!!.extras!!.remove("open_recently_installed")
- startActivity(InstalledActivity.intent(importMode = false, context = this))
- finish()
- return
- }
-
- if (extras.containsKey(WatchListStateViewModel.EXTRA_TAG_ID)) {
- val extraTagId = extras.getInt(WatchListStateViewModel.EXTRA_TAG_ID)
- intent!!.extras!!.remove(WatchListStateViewModel.EXTRA_TAG_ID)
- startActivity(TagWatchListComposeActivity.createTagIntent(
- tag = Tag(
- id = extraTagId,
- name = "",
- color = extras.getInt(WatchListStateViewModel.EXTRA_TAG_COLOR)
- ),
- context = this
- ))
- // Do not finish and return to support back action
- }
-
- accountSelectionDialog = AccountSelectionDialog(this, prefs)
- notificationPermissionRequest = registerForActivityResult(AppPermissions.Request()) {
- val enabled = it[AppPermission.PostNotification.value] ?: false
- mainViewModel.handleEvent(MainViewEvent.NotificationPermissionResult(enabled = enabled))
- }
-
- onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- if (mainViewModel.viewState.isDrawerOpen) {
- mainViewModel.handleEvent(MainViewEvent.DrawerState(isOpen = false))
- } else {
- listViewModel.handleEvent(WatchListEvent.NavigationButton)
- }
- }
- })
-
- setContent {
- AppTheme(
- theme = prefs.theme,
- transparentSystemUi = true
- ) {
- val mainState by mainViewModel.viewStates.collectAsState(initial = mainViewModel.viewState)
- val listState by listViewModel.viewStates.collectAsState(initial = listViewModel.viewState)
-
- val pagingSourceConfig = WatchListPagingSource.Config(
- filterId = listState.filterId,
- tagId = null,
- showRecentlyDiscovered = prefs.showRecentlyDiscovered,
- showOnDevice = prefs.showOnDevice,
- showRecentlyInstalled = prefs.showRecent
- )
-
- val drawerValue = if (mainState.isDrawerOpen) DrawerValue.Open else DrawerValue.Closed
- val drawerState = rememberDrawerState(initialValue = drawerValue)
- LaunchedEffect(true) {
- repeatOnLifecycle(state = Lifecycle.State.RESUMED) {
- mainViewModel.viewActions.collect { action ->
- if (action is MainViewAction.DrawerState) {
- if (action.isOpen) {
- drawerState.open()
- } else {
- drawerState.close()
- }
- } else {
- onMainAction(action)
- }
- }
- }
- }
-
- if (listState.wideLayout.isWideLayout) {
- MainDetailScreen(
- wideLayout = listState.wideLayout,
- main = {
- MainScreen(
- mainState = mainState,
- drawerState = drawerState,
- onMainEvent = mainViewModel::handleEvent,
- listState = listState,
- pagingSourceConfig = pagingSourceConfig,
- onListEvent = listViewModel::handleEvent,
- installedApps = listViewModel.installedApps
- )
- },
- detail = {
- DetailContent(
- app = listState.selectedApp,
- onDismissRequest = { listViewModel.handleEvent(WatchListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- )
- } else {
- MainScreen(
- mainState = mainState,
- drawerState = drawerState,
- onMainEvent = mainViewModel::handleEvent,
- listState = listState,
- pagingSourceConfig = pagingSourceConfig,
- onListEvent = listViewModel::handleEvent,
- installedApps = listViewModel.installedApps
- )
- if (listState.selectedApp != null) {
- DetailsDialog(
- app = listState.selectedApp!!,
- onDismissRequest = { listViewModel.handleEvent(WatchListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- }
- }
- }
-
- lifecycleScope.launch {
- listViewModel.viewActions.collect { onCommonActivityAction(it) }
- }
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- foldableDevice.layout.collect {
- listViewModel.handleEvent(WatchListEvent.SetWideLayout(it))
- }
- }
- }
-
- lifecycleScope.launch {
- accountSelectionDialog.accountSelected.collect { result ->
- mainViewModel.handleEvent(MainViewEvent.SetAccount(result))
- }
- }
-
- val account = prefs.account?.toAndroidAccount()
- if (account == null) {
- Toast.makeText(this, R.string.failed_gain_access, Toast.LENGTH_LONG).show()
- accountSelectionDialog.show()
- }
- }
-
- override fun onResume() {
- super.onResume()
-
- mainViewModel.handleEvent(MainViewEvent.InitAccount)
- AppLog.d("mark updates as viewed.")
- prefs.isLastUpdatesViewed = true
- }
-
- private fun onMainAction(action: MainViewAction) {
- when (action) {
- is MainViewAction.NavigateTo -> {
- when (action.id) {
- DrawerItem.Id.Add -> startActivity(Intent(this, MarketSearchActivity::class.java))
- DrawerItem.Id.Installed -> startActivity(InstalledActivity.intent(false, this))
- DrawerItem.Id.Refresh -> { }
- DrawerItem.Id.Settings -> startActivity(Intent(this, SettingsActivity::class.java))
- DrawerItem.Id.Wishlist -> startActivity(WishListActivity.intent(this))
- DrawerItem.Id.Purchases -> startActivity(HistoryListActivity.intent(this))
- }
- }
- is MainViewAction.NavigateToTag -> startActivity(TagWatchListComposeActivity.createTagIntent(action.tag, this))
- MainViewAction.RequestNotificationPermission -> notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput())
- MainViewAction.ChooseAccount -> accountSelectionDialog.show()
- is MainViewAction.ActivityAction -> onCommonActivityAction(action.action)
- is MainViewAction.DrawerState -> { }
- }
- }
-
- companion object {
- const val EXTRA_FROM_NOTIFICATION = "extra_noti"
- const val EXTRA_EXPAND_SEARCH = "expand_search"
-
- const val ARG_FILTER = "filter"
- const val ARG_SORT = "sort"
- const val ARG_TAG = "tag"
- const val ARG_SHOW_ACTION = "showAction"
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt
index 803d9893..e651d185 100644
--- a/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt
+++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt
@@ -1,22 +1,153 @@
package com.anod.appwatcher.watchlist
+import android.accounts.Account
+import android.content.Context
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.ActivityResultLauncher
import androidx.compose.material3.DrawerState
+import androidx.compose.material3.DrawerValue
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Text
+import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LifecycleEventEffect
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation3.runtime.NavKey
import com.anod.appwatcher.R
+import com.anod.appwatcher.accounts.AccountSelectionRequest
+import com.anod.appwatcher.accounts.AccountSelectionResult
import com.anod.appwatcher.compose.FilterMenuAction
import com.anod.appwatcher.compose.OpenDrawerIcon
import com.anod.appwatcher.compose.PlayStoreMyAppsIcon
import com.anod.appwatcher.compose.RefreshIcon
import com.anod.appwatcher.compose.SortMenuItem
import com.anod.appwatcher.database.entities.Tag
+import com.anod.appwatcher.navigation.SceneNavKey
+import com.anod.appwatcher.preferences.Preferences
import com.anod.appwatcher.tags.EditTagDialog
+import info.anodsplace.framework.app.FoldableDeviceLayout
import info.anodsplace.framework.content.InstalledApps
+import info.anodsplace.framework.content.showToast
+import info.anodsplace.framework.content.startActivity
+import info.anodsplace.permissions.AppPermission
+import info.anodsplace.permissions.AppPermissions
+import info.anodsplace.permissions.toRequestInput
+@Composable
+fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, navigateBack: () -> Unit, navigateTo: (NavKey) -> Unit) {
+ val mainViewModel: MainViewModel = viewModel()
+ val listViewModel: WatchListStateViewModel = viewModel(factory =
+ WatchListStateViewModel.Factory(
+ defaultFilterId = prefs.defaultMainFilterId,
+ wideLayout = wideLayout,
+ collectRecentlyInstalledApps = prefs.showRecent,
+ initialTag = Tag.empty
+ ),
+ key = SceneNavKey.Main.toString()
+ )
+
+ val mainState by mainViewModel.viewStates.collectAsState(initial = mainViewModel.viewState)
+ val listState by listViewModel.viewStates.collectAsState(initial = listViewModel.viewState)
+ val drawerValue = if (mainState.isDrawerOpen) DrawerValue.Open else DrawerValue.Closed
+ val drawerState = rememberDrawerState(initialValue = drawerValue)
+ val context = LocalContext.current
+
+ val notificationPermissionRequest = rememberLauncherForActivityResult(AppPermissions.Request()) {
+ val enabled = it[AppPermission.PostNotification.value] ?: false
+ mainViewModel.handleEvent(MainViewEvent.NotificationPermissionResult(enabled))
+ }
+
+ val accountSelectionRequest = rememberLauncherForActivityResult(AccountSelectionRequest()) {
+ mainViewModel.handleEvent(MainViewEvent.SetAccount(it))
+ }
+
+ LaunchedEffect(true) {
+ mainViewModel.viewActions.collect { action ->
+ if (action is MainViewAction.DrawerState) {
+ if (action.isOpen) {
+ drawerState.open()
+ } else {
+ drawerState.close()
+ }
+ } else {
+ onMainAction(
+ action = action,
+ context = context,
+ accountSelectionRequest = accountSelectionRequest,
+ notificationPermissionRequest = notificationPermissionRequest,
+ navigateTo = navigateTo
+ )
+ }
+ }
+ }
+ LaunchedEffect(true) {
+ listViewModel.viewActions.collect { action ->
+ when (action) {
+ is WatchListAction.StartActivity -> context.startActivity(action)
+ is WatchListAction.ShowToast -> context.showToast(action)
+ is WatchListAction.SelectApp -> navigateTo(SceneNavKey.AppDetails(action.app))
+ WatchListAction.NavigateBack -> navigateBack()
+ is WatchListAction.NavigateTo -> navigateTo(action.navKey)
+ }
+ }
+ }
+ val pagingSourceConfig = WatchListPagingSource.Config(
+ filterId = listState.filterId,
+ tagId = null,
+ showRecentlyDiscovered = prefs.showRecentlyDiscovered,
+ showOnDevice = prefs.showOnDevice,
+ showRecentlyInstalled = prefs.showRecent
+ )
+ MainScreen(
+ mainState = mainState,
+ drawerState = drawerState,
+ onMainEvent = mainViewModel::handleEvent,
+ listState = listState,
+ pagingSourceConfig = pagingSourceConfig,
+ onListEvent = listViewModel::handleEvent,
+ installedApps = listViewModel.installedApps
+ )
+
+ LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
+ mainViewModel.handleEvent(MainViewEvent.OnResume)
+ }
+}
+
+private fun onMainAction(
+ action: MainViewAction,
+ context: Context,
+ accountSelectionRequest: ManagedActivityResultLauncher,
+ notificationPermissionRequest: ActivityResultLauncher,
+ navigateTo: (NavKey) -> Unit
+) {
+ when (action) {
+ is MainViewAction.NavigateTo -> {
+ when (action.id) {
+ DrawerItem.Id.Add -> navigateTo(SceneNavKey.Search())
+ DrawerItem.Id.Installed -> navigateTo(SceneNavKey.Installed(importMode = false))
+ DrawerItem.Id.Refresh -> {}
+ DrawerItem.Id.Settings -> navigateTo(SceneNavKey.Settings)
+ DrawerItem.Id.Wishlist -> navigateTo(SceneNavKey.WishList)
+ DrawerItem.Id.Purchases -> navigateTo(SceneNavKey.PurchaseHistory)
+ }
+ }
+ is MainViewAction.NavigateToTag -> navigateTo(SceneNavKey.TagWatchList(tag = action.tag))
+ MainViewAction.RequestNotificationPermission -> notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput())
+ is MainViewAction.ChooseAccount -> accountSelectionRequest.launch(action.currentAccount)
+ is MainViewAction.ShowToast -> context.showToast(action)
+ is MainViewAction.DrawerState -> { }
+ is MainViewAction.StartActivity -> context.startActivity(action)
+ }
+}
@Composable
fun MainScreen(
mainState: MainViewState,
@@ -56,7 +187,7 @@ fun MainScreen(
subtitle = subtitle,
filterId = filterId,
onListEvent = {
- if (it is WatchListEvent.NavigationButton) {
+ if (it is WatchListEvent.OnBackPressed) {
if (listState.showSearch) {
onListEvent(WatchListEvent.ShowSearch(show = false))
} else {
diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/MainViewModel.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainViewModel.kt
index 02f34a32..b104ff54 100644
--- a/app/src/main/java/com/anod/appwatcher/watchlist/MainViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainViewModel.kt
@@ -24,7 +24,8 @@ import com.anod.appwatcher.utils.networkConnection
import com.anod.appwatcher.utils.prefs
import com.google.firebase.crashlytics.FirebaseCrashlytics
import info.anodsplace.applog.AppLog
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.content.ShowToastActionDefaults
+import info.anodsplace.framework.content.StartActivityAction
import info.anodsplace.ktx.Hash
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -53,37 +54,29 @@ sealed interface MainViewEvent {
class SetAccount(val result: AccountSelectionResult) : MainViewEvent
class NavigateToTag(val tag: Tag) : MainViewEvent
class NotificationPermissionResult(val enabled: Boolean) : MainViewEvent
- data object InitAccount : MainViewEvent
+ data object OnResume : MainViewEvent
class DrawerState(val isOpen: Boolean) : MainViewEvent
}
sealed interface MainViewAction {
- class ActivityAction(val action: CommonActivityAction) : MainViewAction
- data object ChooseAccount : MainViewAction
+ class StartActivity(override val intent: Intent) : MainViewAction, StartActivityAction
+ class ShowToast(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT) : ShowToastActionDefaults(resId, text, length), MainViewAction
+ data class ChooseAccount(val currentAccount: Account?) : MainViewAction
class NavigateTo(val id: DrawerItem.Id) : MainViewAction
data object RequestNotificationPermission : MainViewAction
class NavigateToTag(val tag: Tag) : MainViewAction
class DrawerState(val isOpen: Boolean) : MainViewAction
}
-private fun startActivityAction(intent: Intent, addMultiWindowFlags: Boolean = false): MainViewAction.ActivityAction {
- return MainViewAction.ActivityAction(
- action = CommonActivityAction.StartActivity(
- intent = intent,
- addMultiWindowFlags = addMultiWindowFlags
- )
- )
-}
+private fun startActivityAction(intent: Intent): MainViewAction
+ = MainViewAction.StartActivity(intent)
-private fun showToastAction(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): MainViewAction.ActivityAction {
- return MainViewAction.ActivityAction(
- action = CommonActivityAction.ShowToast(
- resId = resId,
- text = text,
- length = length
- )
+private fun showToastAction(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): MainViewAction
+ = MainViewAction.ShowToast(
+ resId = resId,
+ text = text,
+ length = length
)
-}
class MainViewModel : BaseFlowViewModel(), KoinComponent {
private val database: AppsDatabase by inject()
@@ -126,6 +119,11 @@ class MainViewModel : BaseFlowViewModel {
when (event.result) {
AccountSelectionResult.Canceled -> if (viewState.account == null) {
- onAccountNotFound("")
+ showAccountErrorToast("")
}
- is AccountSelectionResult.Error -> onAccountNotFound(event.result.errorMessage)
+
+ is AccountSelectionResult.Error -> showAccountErrorToast(event.result.errorMessage)
is AccountSelectionResult.Success -> {
onAccountSelect(event.result.account)
}
@@ -148,9 +147,9 @@ class MainViewModel : BaseFlowViewModel emitAction(MainViewAction.NavigateToTag(tag = event.tag))
- MainViewEvent.ChooseAccount -> emitAction(MainViewAction.ChooseAccount)
+ MainViewEvent.ChooseAccount -> emitAction(MainViewAction.ChooseAccount(prefs.account?.toAndroidAccount()))
is MainViewEvent.NotificationPermissionResult -> onNotificationResult(event.enabled)
- is MainViewEvent.InitAccount -> initAccount()
+ is MainViewEvent.OnResume -> onResume()
is MainViewEvent.DrawerState -> {
viewState = viewState.copy(isDrawerOpen = event.isOpen)
emitAction(MainViewAction.DrawerState(isOpen = event.isOpen))
@@ -158,6 +157,12 @@ class MainViewModel : BaseFlowViewModel(), KoinComponent {
+) : BaseFlowViewModel(), KoinComponent {
private val authToken: AuthTokenBlocking by inject()
private val application: Application by inject()
private val db: AppsDatabase by inject()
@@ -138,21 +145,18 @@ class WatchListStateViewModel(
val installedApps = InstalledApps.MemoryCache(InstalledApps.PackageManager(packageManager))
- companion object {
- const val EXTRA_TAG = "extra_tag"
- const val EXTRA_TAG_ID = "tag_id"
- const val EXTRA_TAG_COLOR = "tag_color"
- }
-
class Factory(
private val defaultFilterId: Int,
private val wideLayout: FoldableDeviceLayout,
- private val collectRecentlyInstalledApps: Boolean
- ) : AbstractSavedStateViewModelFactory() {
+ private val collectRecentlyInstalledApps: Boolean,
+ private val initialTag: Tag
+ ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
- override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T {
+ override fun create(modelClass: KClass, extras: CreationExtras): T {
+ val state = extras.createSavedStateHandle()
return WatchListStateViewModel(
- state = handle,
+ state = state,
+ tag = initialTag,
defaultFilterId = defaultFilterId,
wideLayout = wideLayout,
collectRecentlyInstalledApps = collectRecentlyInstalledApps
@@ -164,13 +168,6 @@ class WatchListStateViewModel(
val expandSearch = state.remove("expand_search") ?: false
val fromNotification = state.remove("extra_noti") ?: false
val filterId = if (fromNotification || expandSearch) defaultFilterId else state.getInt("tab_id", defaultFilterId)
- val extraTag: Tag? = state[EXTRA_TAG]
- val tag: Tag = if (extraTag == null) {
- val extraTagId: Int = state[EXTRA_TAG_ID] ?: 0
- if (extraTagId != 0) Tag(extraTagId, "", state[EXTRA_TAG_COLOR] ?: Tag.DEFAULT_COLOR) else Tag.empty
- } else {
- extraTag
- }
viewState = WatchListSharedState(
tag = tag,
sortId = prefs.sortIndex,
@@ -189,13 +186,18 @@ class WatchListStateViewModel(
}
}
+ if (viewState.tag.isEmpty) {
+ AppLog.d("mark updates as viewed.")
+ prefs.isLastUpdatesViewed = true
+ }
+
if (!viewState.tag.isEmpty) {
viewModelScope.launch {
db.tags()
.observeTag(viewState.tag.id)
.collect { tag ->
if (tag == null) {
- emitAction(CommonActivityAction.Finish)
+ // TODO: emitAction(CommonActivityAction.Finish.toWatchListAction())
} else {
viewState = viewState.copy(
tag = tag,
@@ -255,31 +257,15 @@ class WatchListStateViewModel(
is WatchListEvent.FilterById -> viewState = viewState.copy(filterId = event.filterId)
is WatchListEvent.EditTag -> viewState = viewState.copy(showEditTagDialog = event.show)
is WatchListEvent.ShowSearch -> viewState = viewState.copy(showSearch = event.show)
- is WatchListEvent.SelectApp -> viewState = viewState.copy(selectedApp = event.app)
-
- WatchListEvent.NavigationButton -> {
- if (viewState.showSearch) {
- viewState = viewState.copy(showSearch = false, titleFilter = "")
- } else if (viewState.wideLayout.isWideLayout && viewState.selectedApp != null) {
- viewState = viewState.copy(selectedApp = null)
- } else {
- emitAction(CommonActivityAction.Finish)
- }
- }
+ is WatchListEvent.SelectApp -> emitAction(WatchListAction.SelectApp(event.app))
+ WatchListEvent.OnBackPressed -> emitAction(WatchListAction.NavigateBack)
is WatchListEvent.SearchSubmit -> {
val query = viewState.titleFilter
viewState = viewState.copy(showSearch = false, titleFilter = "")
- emitAction(
- startActivityAction(
- intent = MarketSearchActivity.intent(
- application,
- query,
- true,
- initiateSearch = true
- )
- )
- )
+ emitAction(WatchListAction.NavigateTo(
+ SceneNavKey.Search(keyword = query, focus = true, initiateSearch = true)
+ ))
}
is WatchListEvent.UpdateSyncProgress -> {
@@ -294,25 +280,18 @@ class WatchListStateViewModel(
}
WatchListEvent.PlayStoreMyApps -> emitAction(startActivityAction(
intent = Intent().forMyApps(true),
- addMultiWindowFlags = true
))
WatchListEvent.Refresh -> refresh()
is WatchListEvent.AppClick -> {
- viewState = viewState.copy(selectedApp = event.app)
+ emitAction(WatchListAction.SelectApp(event.app))
}
is WatchListEvent.AppLongClick -> {}
is WatchListEvent.EmptyButton -> {
when (event.idx) {
- 1 -> emitAction(startActivityAction(
- intent = MarketSearchActivity.intent(
- application,
- keyword = "",
- focus = true,
- )
- ))
- 2 -> emitAction(startActivityAction(
- intent = InstalledActivity.intent(importMode = true, application)
+ 1 -> emitAction(WatchListAction.NavigateTo(
+ SceneNavKey.Search(focus = true,)
))
+ 2 -> emitAction(WatchListAction.NavigateTo(SceneNavKey.Installed(importMode = true)))
3 -> emitAction(startActivityAction(
intent = Intent.makeMainActivity(ComponentName("com.android.vending", "com.android.vending.AssetBrowserActivity"))
))
@@ -320,8 +299,8 @@ class WatchListStateViewModel(
}
is WatchListEvent.SectionHeaderClick -> {
when (event.type) {
- SectionHeader.RecentlyInstalled -> emitAction(startActivityAction(
- intent = InstalledActivity.intent(importMode = false, application)
+ SectionHeader.RecentlyInstalled -> emitAction(WatchListAction.NavigateTo(
+ SceneNavKey.Installed(importMode = false)
))
else -> { }
}
@@ -332,7 +311,7 @@ class WatchListStateViewModel(
}
private fun pinTagShortcut() {
- val intent = AppWatcherActivity.createTagShortcutIntent(viewState.tag.id, viewState.tag.color, application)
+ val intent = AppWatcherActivity.tagShortcutIntent(viewState.tag.id, viewState.tag.color, application)
viewModelScope.launch {
try {
val icon = createTagIcon()
@@ -344,7 +323,7 @@ class WatchListStateViewModel(
))
} catch (e: Exception) {
AppLog.e(e)
- emitAction(CommonActivityAction.ShowToast(
+ emitAction(showToastAction(
resId = R.string.unable_pin_shortcut,
length = Toast.LENGTH_SHORT
))
@@ -354,7 +333,7 @@ class WatchListStateViewModel(
private suspend fun createTagIcon(): Icon = withContext(Dispatchers.Default) {
val roles = MaterialColors.getColorRoles(viewState.tag.color, false)
- val background = ColorDrawable(roles.accent)
+ val background = roles.accent.toDrawable()
val foreground: Drawable = ContextCompat.getDrawable(application, R.drawable.shortcut_tag)!!
foreground.setTint(roles.onAccent)
return@withContext AdaptiveIconDrawable(background, foreground).toIcon(application)
@@ -366,14 +345,14 @@ class WatchListStateViewModel(
appScope.launch {
try {
requestRefresh()
- } catch (e: Exception) {
+ } catch (_: Exception) {
if (networkConnection.isNetworkAvailable) {
- emitAction(CommonActivityAction.ShowToast(
+ emitAction(showToastAction(
resId = R.string.failed_gain_access,
length = Toast.LENGTH_SHORT
))
} else {
- emitAction(CommonActivityAction.ShowToast(
+ emitAction(showToastAction(
resId = R.string.check_connection,
length = Toast.LENGTH_SHORT
))
diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListTopBar.kt b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListTopBar.kt
index a60132eb..cfad31e3 100644
--- a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListTopBar.kt
+++ b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListTopBar.kt
@@ -57,7 +57,7 @@ fun WatchListTopBar(
onEvent = { event ->
when (event) {
SearchTopBarEvent.NavigationAction -> {
- onEvent(WatchListEvent.NavigationButton)
+ onEvent(WatchListEvent.OnBackPressed)
}
SearchTopBarEvent.SearchAction -> {
onEvent(WatchListEvent.ShowSearch(show = true))
diff --git a/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt b/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt
deleted file mode 100644
index ae728cd6..00000000
--- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.anod.appwatcher.wishlist
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.lifecycle.lifecycleScope
-import com.anod.appwatcher.R
-import com.anod.appwatcher.compose.AppTheme
-import com.anod.appwatcher.compose.BaseComposeActivity
-import com.anod.appwatcher.compose.MainDetailScreen
-import com.anod.appwatcher.details.DetailsDialog
-import com.anod.appwatcher.utils.prefs
-import com.anod.appwatcher.watchlist.DetailContent
-import info.anodsplace.framework.content.onCommonActivityAction
-import kotlinx.coroutines.launch
-import org.koin.core.component.KoinComponent
-
-/**
- * @author Alex Gavrishev
- * *
- * @date 16/12/2016.
- */
-class WishListActivity : BaseComposeActivity(), KoinComponent {
-
- private val viewModel: WishListViewModel by viewModels(factoryProducer = {
- WishListViewModel.Factory(
- wideLayout = foldableDevice.layout.value,
- )
- })
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (!viewModel.authenticated) {
- Toast.makeText(this, R.string.choose_an_account, Toast.LENGTH_SHORT).show()
- finish()
- return
- }
-
- setContent {
- AppTheme(
- theme = viewModel.prefs.theme
- ) {
- val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
- if (screenState.wideLayout.isWideLayout) {
- MainDetailScreen(
- wideLayout = screenState.wideLayout,
- main = {
- WishListScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent,
- pagingDataFlow = viewModel.pagingData,
- viewActions = viewModel.viewActions,
- onActivityAction = { onCommonActivityAction(it) }
- )
- },
- detail = {
- DetailContent(
- app = screenState.selectedApp,
- onDismissRequest = { viewModel.handleEvent(WishListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- )
- } else {
- WishListScreen(
- screenState = screenState,
- onEvent = viewModel::handleEvent,
- pagingDataFlow = viewModel.pagingData,
- viewActions = viewModel.viewActions,
- onActivityAction = { onCommonActivityAction(it) }
- )
- if (screenState.selectedApp != null) {
- DetailsDialog(
- app = screenState.selectedApp!!,
- onDismissRequest = { viewModel.handleEvent(WishListEvent.SelectApp(app = null)) },
- onCommonActivityAction = { onCommonActivityAction(it) }
- )
- }
- }
- }
- }
-
- lifecycleScope.launch {
- foldableDevice.layout.collect {
- viewModel.handleEvent(WishListEvent.SetWideLayout(it))
- }
- }
- }
-
- companion object {
- fun intent(context: Context): Intent = Intent(context, WishListActivity::class.java)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/anod/appwatcher/wishlist/WishListScreen.kt b/app/src/main/java/com/anod/appwatcher/wishlist/WishListScreen.kt
index dee072cd..c64aacdb 100644
--- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListScreen.kt
+++ b/app/src/main/java/com/anod/appwatcher/wishlist/WishListScreen.kt
@@ -20,10 +20,10 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -31,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
@@ -39,23 +40,43 @@ import androidx.paging.compose.itemKey
import com.anod.appwatcher.R
import com.anod.appwatcher.compose.SearchTopBar
import com.anod.appwatcher.database.entities.App
+import com.anod.appwatcher.navigation.SceneNavKey
import com.anod.appwatcher.search.ListItem
import com.anod.appwatcher.search.MarketAppItem
import com.anod.appwatcher.search.RetryButton
import com.anod.appwatcher.tags.TagSelectionDialog
-import com.anod.appwatcher.tags.TagSnackbar
+import com.anod.appwatcher.tags.TagSnackbar.Visuals
import com.anod.appwatcher.utils.AppIconLoader
-import info.anodsplace.framework.content.CommonActivityAction
+import info.anodsplace.framework.app.FoldableDeviceLayout
+import info.anodsplace.framework.content.startActivity
import kotlinx.coroutines.flow.Flow
import org.koin.java.KoinJavaComponent
+@Composable
+fun WishListScreenScene(wideLayout: FoldableDeviceLayout, navigateBack: () -> Unit) {
+ val viewModel: WishListViewModel = viewModel(
+ factory = WishListViewModel.Factory(
+ wideLayout = wideLayout,
+ ),
+ key = SceneNavKey.WishList.toString()
+ )
+ val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState)
+ WishListScreen(
+ screenState = screenState,
+ onEvent = viewModel::handleEvent,
+ pagingDataFlow = viewModel.pagingData,
+ viewActions = viewModel.viewActions,
+ navigateBack = navigateBack
+ )
+}
+
@Composable
fun WishListScreen(
screenState: WishListState,
pagingDataFlow: Flow>,
onEvent: (WishListEvent) -> Unit,
viewActions: Flow,
- onActivityAction: (CommonActivityAction) -> Unit,
+ navigateBack: () -> Unit = {},
appIconLoader: AppIconLoader = KoinJavaComponent.getKoin().get(),
) {
val context = LocalContext.current
@@ -86,28 +107,36 @@ fun WishListScreen(
.padding(paddingValues)
.fillMaxSize(),
) {
- val items = pagingDataFlow.collectAsLazyPagingItems()
- when (items.loadState.refresh) {
- is LoadState.Loading -> {
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
+ if (screenState.isError) {
+ RetryButton(onRetryClick = {
+ onEvent(WishListEvent.RetryClick)
+ })
+ } else {
+ val items = pagingDataFlow.collectAsLazyPagingItems()
+ when (items.loadState.refresh) {
+ is LoadState.Loading -> {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
}
- }
- is LoadState.Error -> {
- RetryButton(onRetryClick = {
- items.refresh()
- })
- }
- is LoadState.NotLoading -> {
- val isEmpty = items.itemCount < 1
- if (isEmpty) {
- WishlistEmpty()
- } else {
- WishlistResults(
- items = items,
- onEvent = onEvent,
- appIconLoader = appIconLoader
- )
+
+ is LoadState.Error -> {
+ RetryButton(onRetryClick = {
+ items.refresh()
+ })
+ }
+
+ is LoadState.NotLoading -> {
+ val isEmpty = items.itemCount < 1
+ if (isEmpty) {
+ WishlistEmpty()
+ } else {
+ WishlistResults(
+ items = items,
+ onEvent = onEvent,
+ appIconLoader = appIconLoader
+ )
+ }
}
}
}
@@ -115,17 +144,18 @@ fun WishListScreen(
}
var showTagList: App? by remember { mutableStateOf(null) }
- val latestOnActivityAction by rememberUpdatedState(onActivityAction)
LaunchedEffect(key1 = viewActions) {
viewActions.collect { action ->
when (action) {
is WishListAction.ShowTagSnackbar -> {
- val result = snackbarHostState.showSnackbar(TagSnackbar.Visuals(action.info, context))
+ val result = snackbarHostState.showSnackbar(Visuals(action.info, context))
if (result == SnackbarResult.ActionPerformed) {
showTagList = action.info
}
}
- is WishListAction.ActivityAction -> latestOnActivityAction(action.action)
+
+ is WishListAction.NavigateBack -> navigateBack()
+ is WishListAction.StartActivity -> context.startActivity(action)
}
}
}
diff --git a/app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt b/app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt
index 58142177..b57fb1af 100644
--- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt
+++ b/app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt
@@ -1,5 +1,6 @@
package com.anod.appwatcher.wishlist
+import android.content.Intent
import android.content.pm.PackageManager
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
@@ -11,6 +12,10 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.filter
+import com.anod.appwatcher.accounts.AuthTokenBlocking
+import com.anod.appwatcher.accounts.CheckTokenError
+import com.anod.appwatcher.accounts.CheckTokenResult
+import com.anod.appwatcher.accounts.toAndroidAccount
import com.anod.appwatcher.database.AppsDatabase
import com.anod.appwatcher.database.entities.App
import com.anod.appwatcher.database.observePackages
@@ -18,18 +23,21 @@ import com.anod.appwatcher.search.ListItem
import com.anod.appwatcher.search.updateRowId
import com.anod.appwatcher.utils.BaseFlowViewModel
import com.anod.appwatcher.utils.date.UploadDateParserCache
+import com.anod.appwatcher.utils.prefs
+import com.anod.appwatcher.wishlist.WishListAction.StartActivity
import finsky.api.DfeApi
import finsky.api.FilterComposite
import finsky.api.FilterPredicate
import info.anodsplace.framework.app.FoldableDeviceLayout
-import info.anodsplace.framework.content.CommonActivityAction
import info.anodsplace.framework.content.InstalledApps
+import info.anodsplace.framework.content.StartActivityAction
import info.anodsplace.playstore.AppDetailsFilter
import info.anodsplace.playstore.AppNameFilter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -38,18 +46,23 @@ data class WishListState(
val nameFilter: String = "",
val wideLayout: FoldableDeviceLayout = FoldableDeviceLayout(),
val selectedApp: App? = null,
+ val isError: Boolean = false
)
sealed interface WishListAction {
class ShowTagSnackbar(val info: App) : WishListAction
- class ActivityAction(val action: CommonActivityAction) : WishListAction
+ class StartActivity(override val intent: Intent) : WishListAction, StartActivityAction
+ data object NavigateBack : WishListAction
}
sealed interface WishListEvent {
data object OnBackPress : WishListEvent
+ data object NoAccount : WishListEvent
+ data object RetryClick : WishListEvent
class OnNameFilter(val query: String) : WishListEvent
class SelectApp(val app: App?) : WishListEvent
class SetWideLayout(val wideLayout: FoldableDeviceLayout) : WishListEvent
+ class AuthTokenError(val error: CheckTokenError) : WishListEvent
}
class WishListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel(), KoinComponent {
@@ -67,6 +80,7 @@ class WishListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel pageData.updateRowId(watchedPackages) }
+ private suspend fun checkAuthToken(): Boolean {
+ val account = prefs.account?.toAndroidAccount()
+ return if (account == null) {
+ handleEvent(WishListEvent.NoAccount)
+ false
+ } else {
+ when (val result = authToken.checkToken(account)) {
+ is CheckTokenResult.Error -> {
+ handleEvent(WishListEvent.AuthTokenError(result.error))
+ false
+ }
+
+ is CheckTokenResult.Success -> true
+ }
+ }
+ }
+
override fun handleEvent(event: WishListEvent) {
when (event) {
- WishListEvent.OnBackPress -> emitAction(WishListAction.ActivityAction(CommonActivityAction.Finish))
+ WishListEvent.OnBackPress -> emitAction(WishListAction.NavigateBack)
is WishListEvent.OnNameFilter -> viewState = viewState.copy(nameFilter = event.query)
is WishListEvent.SelectApp -> {
viewState = viewState.copy(selectedApp = event.app)
@@ -124,6 +159,18 @@ class WishListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel {
viewState = viewState.copy(wideLayout = event.wideLayout)
}
+
+ is WishListEvent.AuthTokenError -> {
+ viewState = viewState.copy(isError = true)
+ if (event.error is CheckTokenError.RequiresInteraction) {
+ emitAction(StartActivity(event.error.intent))
+ }
+ }
+ WishListEvent.NoAccount -> {
+ viewState = viewState.copy(isError = true)
+ }
+
+ WishListEvent.RetryClick -> viewState = viewState.copy(isError = false)
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 4de010b2..0fe82984 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -16,4 +16,6 @@ plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ktlint.gradle) apply false
+ alias(libs.plugins.kotlinx.serialization) apply false
+ alias(libs.plugins.baselineprofile) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8673c396..78ee7370 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,54 +1,56 @@
[versions]
-activity-compose = "1.10.1"
-agp = "8.10.1"
+activity-compose = "1.11.0"
+agp = "8.13.0"
annotation = "1.9.1"
-ksp = "2.1.21-2.0.1"
-androidx-junit = "1.2.1"
+ksp = "2.3.0"
+androidx-junit = "1.3.0"
appcompat = "1.7.1"
-benchmark-macro-junit4 = "1.3.4"
-coil = "3.2.0"
-compose-bom = "2025.06.00"
-compose-material3 = "1.4.0-alpha15"
+benchmark = "1.4.1"
+coil = "3.3.0"
+compose-bom = "2025.10.01"
+compose-material3 = "1.4.0"
core-splashscreen = "1.0.1"
-core-ktx = "1.16.0"
-espresso-core = "3.6.1"
-firebase-crashlytics = "19.4.4"
-firebase-analytics = "22.4.0"
-firebase-crashlytics-gradle = "3.0.4"
-google-api-client-android = "2.8.0"
-google-api-client = "2.8.0"
-google-services = "4.4.2"
+core-ktx = "1.17.0"
+espresso-core = "3.7.0"
+firebase-crashlytics = "20.0.3"
+firebase-analytics = "23.0.0"
+firebase-crashlytics-gradle = "3.0.6"
+google-api-client-android = "2.8.1"
+google-api-client = "2.8.1"
+google-services = "4.4.4"
junit = "4.13.2"
-koin-core = "4.1.0"
-kotlin = "2.1.21"
+koin-core = "4.1.1"
+kotlin = "2.2.21"
kotlinx-collections-immutable = "0.4.0"
-kotlinx-datetime = "0.6.2"
-ktor = "3.1.3"
-ktlint-gradle = "12.3.0"
-ktlint-compose = "0.4.22"
+kotlinx-datetime = "0.7.1"
+kotlinx-serialization = "1.9.0"
+ktor = "3.3.1"
+ktlint-gradle = "13.1.0"
+ktlint-compose = "0.4.27"
coroutines = "1.10.2"
leakcanary-android = "2.14"
-lifecycle = "2.9.1"
-okhttp = "4.12.0"
-oss-licenses-plugin = "0.10.6"
+lifecycle = "2.9.4"
+navigation3-ui = "1.0.0-SNAPSHOT"
+navigation3-adaptive = "1.0.0-SNAPSHOT"
+okhttp = "5.2.1"
+oss-licenses-plugin = "0.10.9"
paging = "3.3.6"
palette = "1.0.0"
-play-services-auth = "21.3.0"
+play-services-auth = "21.4.0"
play-services-identity = "18.1.0"
-play-services-oss-licenses = "17.1.0"
+play-services-oss-licenses = "17.3.0"
process-phoenix = "3.0.0"
-room = "2.7.1"
-runtime-tracing = "1.8.2"
+room = "2.8.3"
+runtime-tracing = "1.9.4"
uiautomator = "2.3.0"
-window = "1.4.0"
-work-runtime = "2.10.1"
+window = "1.5.0"
+work-runtime = "2.11.0"
[libraries]
-androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
-androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity-compose" }
+androidx-activity = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
-androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmark-macro-junit4" }
+androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmark" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" }
@@ -64,6 +66,8 @@ androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", vers
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
androidx-palette = { module = "androidx.palette:palette", version.ref = "palette" }
androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "palette" }
+androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3-ui" }
+androidx-navigation3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "navigation3-adaptive" }
androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" }
androidx-window = { module = "androidx.window:window", version.ref = "window" }
@@ -84,6 +88,7 @@ google-services = { module = "com.google.gms:google-services", version.ref = "go
junit = { module = "junit:junit", version.ref = "junit" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin-core" }
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinx-collections-immutable" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
@@ -119,10 +124,12 @@ work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "w
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+baselineprofile = { id = "androidx.baselineprofile", version.ref = "benchmark" }
android-library = { id = "com.android.library", version.ref = "agp" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktlint-gradle = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle" }
room = { id = "androidx.room", version.ref = "room" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 0087cd3b..9bbc975c 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 8fc91c82..2e111328 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
\ No newline at end of file
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 7a5952e7..faf93008 100755
--- a/gradlew
+++ b/gradlew
@@ -1,79 +1,129 @@
-#!/usr/bin/env bash
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
-# Use the maximum available, or set MAX_FD != -1 to use that actual.
-MAX_FD="maximum"
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
-warn ( ) {
+warn () {
echo "$*"
-}
+} >&2
-die ( ) {
+die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
-if $cygwin ; then
- [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
-fi
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >&-
-APP_HOME="`pwd -P`"
-cd "$SAVED" >&-
-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -82,83 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, put options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=$((i+1))
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 8a0b282a..9d21a218 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,4 +1,22 @@
-@if "%DEBUG%" == "" @echo off
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -8,26 +26,30 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -35,54 +57,36 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/lib b/lib
index 3583ae33..3da0a8f5 160000
--- a/lib
+++ b/lib
@@ -1 +1 @@
-Subproject commit 3583ae33846fe92040350154e958a3d2f183fcce
+Subproject commit 3da0a8f5aa63d3dd3b1739b35c2ac1a627a6e46b
diff --git a/macrobenchmark/build.gradle.kts b/macrobenchmark/build.gradle.kts
index fa86ede9..bac44a34 100644
--- a/macrobenchmark/build.gradle.kts
+++ b/macrobenchmark/build.gradle.kts
@@ -1,8 +1,16 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
plugins {
id("com.android.test")
alias(libs.plugins.kotlin)
}
+kotlin {
+ compilerOptions {
+ jvmTarget = JvmTarget.JVM_11
+ }
+}
+
android {
namespace = "info.anodpslace.appwatcher.macrobenchmark"
compileSdk = 36
@@ -12,12 +20,8 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}
- kotlinOptions {
- jvmTarget = "11"
- }
-
defaultConfig {
- minSdk = 28
+ minSdk = 31
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
diff --git a/playstore/build.gradle.kts b/playstore/build.gradle.kts
index 8f47477c..58870569 100644
--- a/playstore/build.gradle.kts
+++ b/playstore/build.gradle.kts
@@ -1,13 +1,21 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin)
}
+kotlin {
+ compilerOptions {
+ jvmTarget = JvmTarget.JVM_11
+ }
+}
+
android {
compileSdk = 36
defaultConfig {
- minSdk = 27
+ minSdk = 31
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -16,9 +24,6 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}
- kotlinOptions {
- jvmTarget = "11"
- }
namespace = "info.anodsplace.playstore"
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 76f49e7a..9f39d591 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,6 +3,9 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven {
+ url = uri("https://androidx.dev/snapshots/builds/13647192/artifacts/repository")
+ }
}
}
@@ -11,6 +14,9 @@ pluginManagement {
google()
mavenCentral()
gradlePluginPortal()
+ maven {
+ url = uri("https://androidx.dev/snapshots/builds/13647192/artifacts/repository")
+ }
}
}