From 612f224bac94da63cd4d08d259b4ccfbdf1733b2 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Sun, 15 Jun 2025 22:57:05 +0300 Subject: [PATCH 01/15] Running --- .github/workflows/android.yml | 2 +- app/build.gradle.kts | 3 + .../com/anod/appwatcher/AppWatcherActivity.kt | 68 +++++++++++++++++-- .../appwatcher/compose/BaseComposeActivity.kt | 12 ++++ .../anod/appwatcher/database/entities/App.kt | 2 + .../appwatcher/database/entities/Price.kt | 2 + .../appwatcher/watchlist/DetailContent.kt | 17 +++-- .../anod/appwatcher/watchlist/MainScreen.kt | 59 ++++++++++++++++ build.gradle.kts | 1 + gradle/libs.versions.toml | 8 +++ settings.gradle.kts | 6 ++ 11 files changed, 169 insertions(+), 11 deletions(-) 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..fafe196e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.room) alias(libs.plugins.ktlint.gradle) + alias(libs.plugins.jetbrains.kotlin.serialization) id("kotlin-parcelize") id("com.google.android.gms.oss-licenses-plugin") } @@ -104,6 +105,8 @@ dependencies { 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) diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt index b5b0ec9f..5284daef 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -4,21 +4,81 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import com.anod.appwatcher.watchlist.MainActivity +import androidx.activity.compose.setContent +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.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.BaseComposeActivity +import com.anod.appwatcher.database.entities.App +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.watchlist.WatchListStateViewModel +import info.anodsplace.framework.content.onCommonActivityAction +import kotlinx.serialization.Serializable +import org.koin.core.component.KoinComponent +import androidx.core.net.toUri -class AppWatcherActivity : MainActivity() { +@Serializable +data object MainScreenNavKey : NavKey + +@Serializable +data class SelectedAppNavKey(val selectedApp: App) : NavKey + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +class AppWatcherActivity : BaseComposeActivity(), KoinComponent { override fun onCreate(savedInstanceState: Bundle?) { - setTheme(R.style.AppTheme_Main) super.onCreate(savedInstanceState) + + setContent { + val backStack = rememberNavBackStack(MainScreenNavKey) + val listDetailStrategy = rememberListDetailSceneStrategy() + NavDisplay( + backStack = backStack, + onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, + sceneStrategy = listDetailStrategy, + entryProvider = entryProvider { + entry( + metadata = ListDetailSceneStrategy.listPane( + detailPlaceholder = { + EmptyBoxSmile() + } + ) + ) { + val wideLayout by foldableDevice.layout.collectAsState() + MainScreenScene( + prefs = prefs, + wideLayout = wideLayout + ) + } + entry( + metadata = ListDetailSceneStrategy.detailPane() + ) { key -> + DetailContent( + app = key.selectedApp, + onDismissRequest = { }, + onCommonActivityAction = { onCommonActivityAction(it) } + ) + } + } + ) + } } companion object { fun createTagShortcutIntent(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") + data = "com.anod.appwatcher://tags/$tagId?color=$initialColor".toUri() putExtra(WatchListStateViewModel.EXTRA_TAG_ID, tagId) putExtra(WatchListStateViewModel.EXTRA_TAG_COLOR, initialColor) } 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..1db28835 100644 --- a/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt @@ -1,7 +1,9 @@ package com.anod.appwatcher.compose +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.enableEdgeToEdge import info.anodsplace.framework.app.FoldableDevice abstract class BaseComposeActivity : ComponentActivity() { @@ -9,6 +11,16 @@ abstract class BaseComposeActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { foldableDevice = FoldableDevice.create(this) + setEdgeToEdgeConfig() super.onCreate(savedInstanceState) } +} + +fun ComponentActivity.setEdgeToEdgeConfig() { + enableEdgeToEdge() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // 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/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/watchlist/DetailContent.kt b/app/src/main/java/com/anod/appwatcher/watchlist/DetailContent.kt index 977729fc..ff35f20f 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/DetailContent.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/DetailContent.kt @@ -17,12 +17,7 @@ import info.anodsplace.framework.content.CommonActivityAction fun DetailContent(app: App?, onDismissRequest: () -> Unit, onCommonActivityAction: (action: CommonActivityAction) -> 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, @@ -31,4 +26,14 @@ fun DetailContent(app: App?, onDismissRequest: () -> Unit, onCommonActivityActio ) } } +} + +@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/MainScreen.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt index 803d9893..8bdf226b 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,81 @@ package com.anod.appwatcher.watchlist 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.res.stringResource +import androidx.lifecycle.viewmodel.compose.viewModel import com.anod.appwatcher.R +import com.anod.appwatcher.compose.AppTheme 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.preferences.Preferences import com.anod.appwatcher.tags.EditTagDialog +import info.anodsplace.framework.app.FoldableDeviceLayout import info.anodsplace.framework.content.InstalledApps +@Composable +fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout) { + val mainViewModel: MainViewModel = viewModel() + val listViewModel: WatchListStateViewModel = viewModel(factory = + WatchListStateViewModel.Factory( + defaultFilterId = prefs.defaultMainFilterId, + wideLayout = wideLayout, + collectRecentlyInstalledApps = prefs.showRecent + ) + ) + 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 drawerValue = if (mainState.isDrawerOpen) DrawerValue.Open else DrawerValue.Closed + val drawerState = rememberDrawerState(initialValue = drawerValue) + LaunchedEffect(true) { + mainViewModel.viewActions.collect { action -> + if (action is MainViewAction.DrawerState) { + if (action.isOpen) { + drawerState.open() + } else { + drawerState.close() + } + } else { + // TODO: onMainAction(action) + } + } + } + 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 + ) + } +} + @Composable fun MainScreen( mainState: MainViewState, diff --git a/build.gradle.kts b/build.gradle.kts index 4de010b2..ac844d66 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,4 +16,5 @@ 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.jetbrains.kotlin.serialization) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8673c396..e82ea83e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,12 +23,15 @@ koin-core = "4.1.0" kotlin = "2.1.21" kotlinx-collections-immutable = "0.4.0" kotlinx-datetime = "0.6.2" +kotlinx-serialization = "1.8.1" ktor = "3.1.3" ktlint-gradle = "12.3.0" ktlint-compose = "0.4.22" coroutines = "1.10.2" leakcanary-android = "2.14" lifecycle = "2.9.1" +navigation3-ui = "1.0.0-SNAPSHOT" +navigation3-adaptive = "1.0.0-SNAPSHOT" okhttp = "4.12.0" oss-licenses-plugin = "0.10.6" paging = "3.3.6" @@ -64,6 +67,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" } @@ -89,6 +94,8 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktlint-compose = { group = "io.nlopez.compose.rules", name = "ktlint", version.ref = "ktlint-compose" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary-android" } @@ -123,6 +130,7 @@ 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" } +jetbrains-kotlin-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/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") + } } } From 241b2f30787997ef60a6300bdb61f830978e3fcf Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Fri, 20 Jun 2025 14:41:18 +0300 Subject: [PATCH 02/15] wip --- .../com/anod/appwatcher/AppWatcherActivity.kt | 94 ++++++++++------ .../anod/appwatcher/database/entities/Tag.kt | 2 + .../com/anod/appwatcher/navigation/NavKey.kt | 30 ++++++ .../appwatcher/search/SearchResultsScreen.kt | 22 +++- .../anod/appwatcher/search/SearchViewModel.kt | 23 ++-- .../tags/TagWatchListComposeActivity.kt | 57 +++++----- .../anod/appwatcher/watchlist/MainActivity.kt | 26 ++--- .../anod/appwatcher/watchlist/MainScreen.kt | 100 ++++++++++++------ .../watchlist/WatchListStateViewModel.kt | 69 ++++++------ 9 files changed, 272 insertions(+), 151 deletions(-) create mode 100644 app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt index 5284daef..7ffafe8c 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -2,7 +2,6 @@ package com.anod.appwatcher import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Bundle import androidx.activity.compose.setContent import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @@ -16,61 +15,86 @@ import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import com.anod.appwatcher.compose.BaseComposeActivity -import com.anod.appwatcher.database.entities.App 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.watchlist.WatchListStateViewModel import info.anodsplace.framework.content.onCommonActivityAction -import kotlinx.serialization.Serializable import org.koin.core.component.KoinComponent import androidx.core.net.toUri - -@Serializable -data object MainScreenNavKey : NavKey - -@Serializable -data class SelectedAppNavKey(val selectedApp: App) : NavKey +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavEntry +import com.anod.appwatcher.compose.AppTheme +import com.anod.appwatcher.navigation.MainScreenNavKey +import com.anod.appwatcher.navigation.MarketSearchNavKey +import com.anod.appwatcher.navigation.SelectedAppNavKey +import com.anod.appwatcher.search.SearchResultsScreenScene @OptIn(ExperimentalMaterial3AdaptiveApi::class) class AppWatcherActivity : BaseComposeActivity(), KoinComponent { override fun onCreate(savedInstanceState: Bundle?) { + setTheme(R.style.AppTheme_Main) super.onCreate(savedInstanceState) setContent { val backStack = rememberNavBackStack(MainScreenNavKey) val listDetailStrategy = rememberListDetailSceneStrategy() - NavDisplay( + AppTheme( + theme = prefs.theme, + transparentSystemUi = true + ) { + NavDisplay( + backStack = backStack, + onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, + sceneStrategy = listDetailStrategy, + entryProvider = provideNavEntries(backStack) + ) + } + } + } + + private fun provideNavEntries(backStack: NavBackStack): (NavKey) -> NavEntry = entryProvider { + entry( + metadata = ListDetailSceneStrategy.listPane( + sceneKey = MainScreenNavKey, + detailPlaceholder = { + EmptyBoxSmile() + } + ) + ) { + val wideLayout by foldableDevice.layout.collectAsState() + MainScreenScene( + prefs = prefs, + wideLayout = wideLayout, backStack = backStack, - onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, - sceneStrategy = listDetailStrategy, - entryProvider = entryProvider { - entry( - metadata = ListDetailSceneStrategy.listPane( - detailPlaceholder = { - EmptyBoxSmile() - } - ) - ) { - val wideLayout by foldableDevice.layout.collectAsState() - MainScreenScene( - prefs = prefs, - wideLayout = wideLayout - ) - } - entry( - metadata = ListDetailSceneStrategy.detailPane() - ) { key -> - DetailContent( - app = key.selectedApp, - onDismissRequest = { }, - onCommonActivityAction = { onCommonActivityAction(it) } - ) - } + onCommonActivityAction = { onCommonActivityAction(it) } + ) + } + entry( + metadata = ListDetailSceneStrategy.detailPane() + ) { key -> + DetailContent( + app = key.selectedApp, + onDismissRequest = { }, + onCommonActivityAction = { onCommonActivityAction(it) } + ) + } + entry( + metadata = ListDetailSceneStrategy.listPane( + sceneKey = MarketSearchNavKey, + detailPlaceholder = { + EmptyBoxSmile() } ) + ) { + val wideLayout by foldableDevice.layout.collectAsState() + SearchResultsScreenScene( + wideLayout = wideLayout, + backStack = backStack, + onCommonActivityAction = { onCommonActivityAction(it) } + ) } } 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..3d09ae23 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) diff --git a/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt b/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt new file mode 100644 index 00000000..0b437918 --- /dev/null +++ b/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt @@ -0,0 +1,30 @@ +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 + +@Serializable +data object MainScreenNavKey : NavKey + +@Serializable +data object MarketSearchNavKey : NavKey + +@Serializable +data object SettingsNavKey : NavKey + +@Serializable +data class InstalledNavKey(val importMode: Boolean) : NavKey + +@Serializable +data object WishListNavKey : NavKey + +@Serializable +data object HistoryNavKey : NavKey + +@Serializable +data class TagWatchListNavKey(val tag: Tag) : NavKey + +@Serializable +data class SelectedAppNavKey(val selectedApp: App) : NavKey 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..0de3661c 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,8 @@ 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.NavBackStack import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems @@ -51,12 +54,29 @@ import finsky.api.Document import finsky.protos.AppDetails import finsky.protos.DocDetails import finsky.protos.DocV2 +import info.anodsplace.framework.app.FoldableDeviceLayout import info.anodsplace.framework.content.CommonActivityAction import info.anodsplace.framework.content.InstalledApps import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import org.koin.java.KoinJavaComponent +@Composable +fun SearchResultsScreenScene(wideLayout: FoldableDeviceLayout, backStack: NavBackStack, onCommonActivityAction: (CommonActivityAction) -> Unit) { + val viewModel: SearchViewModel = viewModel(factory = SearchViewModel.Factory( + initialState = SearchViewState(wideLayout = wideLayout), // intentToState(intent, foldableDevice.layout.value), + )) + val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState) + SearchResultsScreen( + screenState = screenState, + onEvent = viewModel::handleEvent, + pagingDataFlow = { viewModel.pagingData }, + viewActions = viewModel.viewActions, + onActivityAction = { onCommonActivityAction(it) }, + onShowAccountDialog = { /* accountSelectionDialog.show() */ } + ) +} + @Composable fun SearchResultsScreen( screenState: SearchViewState, @@ -136,7 +156,7 @@ fun SearchResultsScreen( message = action.message, duration = action.duration ) - if (action.finish) { + if (action.exit) { onActivityAction(CommonActivityAction.Finish) } } 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..08e6c1d4 100644 --- a/app/src/main/java/com/anod/appwatcher/search/SearchViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/search/SearchViewModel.kt @@ -58,10 +58,11 @@ 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 ShowSnackbar(val message: String, val duration: SnackbarDuration = SnackbarDuration.Short, val exit: Boolean = false) : SearchViewAction data object ShowAccountDialog : SearchViewAction class ShowTagSnackbar(val info: App, val isShareSource: Boolean) : SearchViewAction class AlreadyWatchedNotice(val document: Document) : SearchViewAction + data object OnBackPressed: SearchViewAction } private fun startActivityAction(intent: Intent, finish: Boolean = false): SearchViewAction.ActivityAction { @@ -146,26 +147,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.OnBackPressed) 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, exit = 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, exit = 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, exit = true)) } } @@ -180,7 +177,7 @@ class SearchViewModel( SearchViewAction.ShowSnackbar( message = context.getString(R.string.check_connection), duration = SnackbarDuration.Short, - finish = false + exit = false ) ) } else if (searchStatus is SearchStatus.Error) { @@ -188,7 +185,7 @@ class SearchViewModel( SearchViewAction.ShowSnackbar( message = context.getString(R.string.error_fetching_info), duration = SnackbarDuration.Short, - finish = false + exit = false ) ) } @@ -292,9 +289,9 @@ class SearchViewModel( emitAction(startActivityAction(intent = e.intent, 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, exit = 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, exit = true)) } } } diff --git a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt index db57e9fa..7b19e1b8 100644 --- a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt @@ -21,6 +21,7 @@ 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.WatchListAction import com.anod.appwatcher.watchlist.WatchListEvent import com.anod.appwatcher.watchlist.WatchListPagingSource import com.anod.appwatcher.watchlist.WatchListStateViewModel @@ -69,11 +70,11 @@ class TagWatchListComposeActivity : BaseComposeActivity() { ) }, detail = { - DetailContent( - app = screenState.selectedApp, - onDismissRequest = { viewModel.handleEvent(WatchListEvent.SelectApp(app = null)) }, - onCommonActivityAction = { onCommonActivityAction(it) } - ) +// DetailContent( +// app = screenState.selectedApp, +// onDismissRequest = { viewModel.handleEvent(WatchListEvent.SelectApp(app = null)) }, +// onCommonActivityAction = { onCommonActivityAction(it) } +// ) } ) } else { @@ -83,13 +84,13 @@ class TagWatchListComposeActivity : BaseComposeActivity() { 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.selectedApp != null) { +// DetailsDialog( +// app = screenState.selectedApp!!, +// onDismissRequest = { viewModel.handleEvent(WatchListEvent.SelectApp(app = null)) }, +// onCommonActivityAction = { onCommonActivityAction(it) } +// ) +// } } if (screenState.showAppTagDialog) { @@ -109,7 +110,13 @@ class TagWatchListComposeActivity : BaseComposeActivity() { } lifecycleScope.launch { - viewModel.viewActions.collect { onCommonActivityAction(it) } + viewModel.viewActions.collect { + when (it) { + is WatchListAction.ActivityAction -> onCommonActivityAction(it.action) + is WatchListAction.SelectApp -> {} + } + + } } lifecycleScope.launch { @@ -121,18 +128,18 @@ class TagWatchListComposeActivity : BaseComposeActivity() { } } - @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() - } - } +// @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 { diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt index 9eb4969d..de379b44 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt @@ -146,11 +146,11 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { ) }, detail = { - DetailContent( - app = listState.selectedApp, - onDismissRequest = { listViewModel.handleEvent(WatchListEvent.SelectApp(app = null)) }, - onCommonActivityAction = { onCommonActivityAction(it) } - ) +// DetailContent( +// app = listState.selectedApp, +// onDismissRequest = { listViewModel.handleEvent(WatchListEvent.SelectApp(app = null)) }, +// onCommonActivityAction = { onCommonActivityAction(it) } +// ) } ) } else { @@ -163,19 +163,19 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { onListEvent = listViewModel::handleEvent, installedApps = listViewModel.installedApps ) - if (listState.selectedApp != null) { - DetailsDialog( - app = listState.selectedApp!!, - onDismissRequest = { listViewModel.handleEvent(WatchListEvent.SelectApp(app = null)) }, - onCommonActivityAction = { onCommonActivityAction(it) } - ) - } +// 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) } + // listViewModel.viewActions.collect { onCommonActivityAction(it) } } lifecycleScope.launch { 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 8bdf226b..636d6248 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation3.runtime.NavBackStack import com.anod.appwatcher.R import com.anod.appwatcher.compose.AppTheme import com.anod.appwatcher.compose.FilterMenuAction @@ -21,13 +22,21 @@ 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.HistoryNavKey +import com.anod.appwatcher.navigation.InstalledNavKey +import com.anod.appwatcher.navigation.MarketSearchNavKey +import com.anod.appwatcher.navigation.SelectedAppNavKey +import com.anod.appwatcher.navigation.SettingsNavKey +import com.anod.appwatcher.navigation.TagWatchListNavKey +import com.anod.appwatcher.navigation.WishListNavKey import com.anod.appwatcher.preferences.Preferences import com.anod.appwatcher.tags.EditTagDialog import info.anodsplace.framework.app.FoldableDeviceLayout +import info.anodsplace.framework.content.CommonActivityAction import info.anodsplace.framework.content.InstalledApps @Composable -fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout) { +fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backStack: NavBackStack, onCommonActivityAction: (CommonActivityAction) -> Unit) { val mainViewModel: MainViewModel = viewModel() val listViewModel: WatchListStateViewModel = viewModel(factory = WatchListStateViewModel.Factory( @@ -36,46 +45,69 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout) { collectRecentlyInstalledApps = prefs.showRecent ) ) - 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 drawerValue = if (mainState.isDrawerOpen) DrawerValue.Open else DrawerValue.Closed - val drawerState = rememberDrawerState(initialValue = drawerValue) - LaunchedEffect(true) { - mainViewModel.viewActions.collect { action -> - if (action is MainViewAction.DrawerState) { - if (action.isOpen) { - drawerState.open() - } else { - drawerState.close() - } + + 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) + LaunchedEffect(true) { + mainViewModel.viewActions.collect { action -> + if (action is MainViewAction.DrawerState) { + if (action.isOpen) { + drawerState.open() } else { - // TODO: onMainAction(action) + drawerState.close() } + } else { + onMainAction(action, backStack, onCommonActivityAction) + } + } + } + LaunchedEffect(true) { + listViewModel.viewActions.collect { action -> + when (action) { + is WatchListAction.ActivityAction -> { onCommonActivityAction(action.action) } + is WatchListAction.SelectApp -> backStack.add(SelectedAppNavKey(action.app)) } } - 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 - ) } + 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 + ) } +private fun onMainAction(action: MainViewAction, backStack: NavBackStack, onCommonActivityAction: (CommonActivityAction) -> Unit) { + when (action) { + is MainViewAction.NavigateTo -> { + when (action.id) { + DrawerItem.Id.Add -> backStack.add(MarketSearchNavKey) + DrawerItem.Id.Installed -> backStack.add(InstalledNavKey(importMode = false)) + DrawerItem.Id.Refresh -> {} + DrawerItem.Id.Settings -> backStack.add(SettingsNavKey) + DrawerItem.Id.Wishlist -> backStack.add(WishListNavKey) + DrawerItem.Id.Purchases -> backStack.add(HistoryNavKey) + } + } + is MainViewAction.NavigateToTag -> backStack.add(TagWatchListNavKey(tag = action.tag)) + MainViewAction.RequestNotificationPermission -> {} //notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) + MainViewAction.ChooseAccount -> {} //accountSelectionDialog.show() + is MainViewAction.ActivityAction -> onCommonActivityAction(action.action) + is MainViewAction.DrawerState -> { } + } +} @Composable fun MainScreen( mainState: MainViewState, diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt index 6ee9b5ce..128ee116 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.content.pm.PackageManager import android.graphics.Rect import android.graphics.drawable.AdaptiveIconDrawable -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.widget.Toast @@ -61,6 +60,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import androidx.core.graphics.drawable.toDrawable +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.CreationExtras +import kotlin.reflect.KClass @Immutable data class WatchListSharedState( @@ -72,7 +76,6 @@ data class WatchListSharedState( val initialRefreshing: Boolean = false, val syncProgress: SyncProgress? = null, val wideLayout: FoldableDeviceLayout = FoldableDeviceLayout(isWideLayout = false, hinge = Rect()), - val selectedApp: App? = null, val showAppTagDialog: Boolean = false, val showEditTagDialog: Boolean = false, val tagAppsChange: Int = 0, @@ -97,7 +100,7 @@ sealed interface WatchListEvent { class FilterById(val filterId: Int) : WatchListEvent class AddAppToTag(val show: Boolean) : WatchListEvent class EditTag(val show: Boolean) : WatchListEvent - class SelectApp(val app: App?) : WatchListEvent + class SelectApp(val app: App) : WatchListEvent class UpdateSyncProgress(val syncProgress: SyncProgress) : WatchListEvent data object PinTagShortcut : WatchListEvent @@ -107,27 +110,33 @@ sealed interface WatchListEvent { class SectionHeaderClick(val type: SectionHeader) : WatchListEvent } -private fun startActivityAction(intent: Intent, addMultiWindowFlags: Boolean = false): CommonActivityAction.StartActivity { - return CommonActivityAction.StartActivity( +sealed interface WatchListAction { + data class ActivityAction(val action: CommonActivityAction) : WatchListAction + data class SelectApp(val app: App) : WatchListAction +} + +private fun CommonActivityAction.toWatchListAction(): WatchListAction.ActivityAction = WatchListAction.ActivityAction(this) + +private fun startActivityAction(intent: Intent, addMultiWindowFlags: Boolean = false): WatchListAction.ActivityAction + = CommonActivityAction.StartActivity( intent = intent, addMultiWindowFlags = addMultiWindowFlags - ) -} + ).toWatchListAction() -private fun showToastAction(resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): CommonActivityAction.ShowToast { - return CommonActivityAction.ShowToast( + +private fun showToastAction(resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): WatchListAction.ActivityAction + = CommonActivityAction.ShowToast( resId = resId, text = text, length = length - ) -} + ).toWatchListAction() class WatchListStateViewModel( state: SavedStateHandle, defaultFilterId: Int, collectRecentlyInstalledApps: Boolean, wideLayout: FoldableDeviceLayout -) : BaseFlowViewModel(), KoinComponent { +) : BaseFlowViewModel(), KoinComponent { private val authToken: AuthTokenBlocking by inject() private val application: Application by inject() private val db: AppsDatabase by inject() @@ -148,11 +157,11 @@ class WatchListStateViewModel( private val defaultFilterId: Int, private val wideLayout: FoldableDeviceLayout, private val collectRecentlyInstalledApps: Boolean - ) : AbstractSavedStateViewModelFactory() { + ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { + override fun create(modelClass: KClass, extras: CreationExtras): T { return WatchListStateViewModel( - state = handle, + state = extras.createSavedStateHandle(), defaultFilterId = defaultFilterId, wideLayout = wideLayout, collectRecentlyInstalledApps = collectRecentlyInstalledApps @@ -195,7 +204,7 @@ class WatchListStateViewModel( .observeTag(viewState.tag.id) .collect { tag -> if (tag == null) { - emitAction(CommonActivityAction.Finish) + emitAction(CommonActivityAction.Finish.toWatchListAction()) } else { viewState = viewState.copy( tag = tag, @@ -255,16 +264,16 @@ 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) + is WatchListEvent.SelectApp -> emitAction(WatchListAction.SelectApp(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) - } +// 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.SearchSubmit -> { @@ -298,7 +307,7 @@ class WatchListStateViewModel( )) WatchListEvent.Refresh -> refresh() is WatchListEvent.AppClick -> { - viewState = viewState.copy(selectedApp = event.app) + emitAction(WatchListAction.SelectApp(event.app)) } is WatchListEvent.AppLongClick -> {} is WatchListEvent.EmptyButton -> { @@ -344,7 +353,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 +363,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 +375,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 )) From 95b594f176ba834d323f14ce16fe4e32ab01f5ab Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Fri, 20 Jun 2025 14:53:21 +0300 Subject: [PATCH 03/15] commit --- lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib b/lib index 3583ae33..618410c4 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit 3583ae33846fe92040350154e958a3d2f183fcce +Subproject commit 618410c45450f2b00e51834ee502b8a9239ecdab From 178c5653cbfc4be6db443799b0feeaf9d8bbd56e Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Fri, 20 Jun 2025 19:01:43 +0300 Subject: [PATCH 04/15] Remove onCommonActivityAction --- .../com/anod/appwatcher/AppWatcherActivity.kt | 6 +-- .../appwatcher/compose/MainDetailsScreen.kt | 4 +- .../anod/appwatcher/details/DetailsPanel.kt | 22 +++++----- .../appwatcher/details/DetailsViewModel.kt | 36 +++++---------- .../appwatcher/history/HistoryListActivity.kt | 7 +-- .../appwatcher/history/HistoryListScreen.kt | 8 ++-- .../history/HistoryListViewModel.kt | 5 +-- .../appwatcher/installed/InstalledActivity.kt | 8 ++-- .../installed/InstalledListViewModel.kt | 14 +++--- .../preferences/SettingsActivity.kt | 11 +++-- .../preferences/SettingsViewModel.kt | 35 ++++++--------- .../search/SearchComposeActivity.kt | 13 ++---- .../appwatcher/search/SearchResultsScreen.kt | 44 +++++++++---------- .../anod/appwatcher/search/SearchViewModel.kt | 35 +++++++-------- .../sync/SchedulesHistoryActivity.kt | 8 ++-- .../tags/TagWatchListComposeActivity.kt | 8 ++-- .../appwatcher/tags/TagsSelectionViewModel.kt | 5 +-- .../appwatcher/userLog/UserLogActivity.kt | 4 +- .../appwatcher/userLog/UserLogViewModel.kt | 8 ++-- .../appwatcher/utils/ScreenCommonAction.kt | 24 ++++++++++ .../appwatcher/watchlist/DetailContent.kt | 4 +- .../anod/appwatcher/watchlist/MainActivity.kt | 7 +-- .../anod/appwatcher/watchlist/MainScreen.kt | 19 +++++--- .../appwatcher/watchlist/MainViewModel.kt | 29 +++++------- .../watchlist/WatchListStateViewModel.kt | 26 +++++------ .../appwatcher/wishlist/WishListActivity.kt | 5 --- .../appwatcher/wishlist/WishListScreen.kt | 7 ++- .../appwatcher/wishlist/WishListViewModel.kt | 5 +-- lib | 2 +- 29 files changed, 190 insertions(+), 219 deletions(-) create mode 100644 app/src/main/java/com/anod/appwatcher/utils/ScreenCommonAction.kt diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt index 7ffafe8c..f56ffb9e 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -20,7 +20,6 @@ import com.anod.appwatcher.watchlist.DetailContent import com.anod.appwatcher.watchlist.EmptyBoxSmile import com.anod.appwatcher.watchlist.MainScreenScene import com.anod.appwatcher.watchlist.WatchListStateViewModel -import info.anodsplace.framework.content.onCommonActivityAction import org.koin.core.component.KoinComponent import androidx.core.net.toUri import androidx.navigation3.runtime.NavBackStack @@ -69,7 +68,6 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { prefs = prefs, wideLayout = wideLayout, backStack = backStack, - onCommonActivityAction = { onCommonActivityAction(it) } ) } entry( @@ -78,7 +76,6 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { DetailContent( app = key.selectedApp, onDismissRequest = { }, - onCommonActivityAction = { onCommonActivityAction(it) } ) } entry( @@ -92,8 +89,7 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { val wideLayout by foldableDevice.layout.collectAsState() SearchResultsScreenScene( wideLayout = wideLayout, - backStack = backStack, - onCommonActivityAction = { onCommonActivityAction(it) } + navigateBack = { backStack.removeLastOrNull() }, ) } } diff --git a/app/src/main/java/com/anod/appwatcher/compose/MainDetailsScreen.kt b/app/src/main/java/com/anod/appwatcher/compose/MainDetailsScreen.kt index 68147dd1..7051af40 100644 --- a/app/src/main/java/com/anod/appwatcher/compose/MainDetailsScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/compose/MainDetailsScreen.kt @@ -46,8 +46,8 @@ 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 = {}) }, + main = { DetailContent(app = null, onDismissRequest = {}) }, + detail = { DetailContent(app = null, onDismissRequest = {}) }, ) } } \ No newline at end of file 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..e77b64ee 100644 --- a/app/src/main/java/com/anod/appwatcher/details/DetailsPanel.kt +++ b/app/src/main/java/com/anod/appwatcher/details/DetailsPanel.kt @@ -113,7 +113,8 @@ 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 @@ -126,7 +127,7 @@ 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) { +fun DetailsPanel(app: App, onDismissRequest: () -> Unit) { val storeOwner = rememberViwModeStoreOwner() val isSystemInDarkTheme = isSystemInDarkTheme() val viewModel: DetailsViewModel = viewModel( @@ -150,13 +151,12 @@ 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) { +fun DetailsDialog(app: App, onDismissRequest: () -> Unit) { val storeOwner = rememberViwModeStoreOwner() val isSystemInDarkTheme = isSystemInDarkTheme() val viewModel: DetailsViewModel = viewModel( @@ -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..cdb73163 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(@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 index f6018f47..ff54f04a 100644 --- a/app/src/main/java/com/anod/appwatcher/history/HistoryListActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/history/HistoryListActivity.kt @@ -18,7 +18,6 @@ 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 @@ -56,15 +55,13 @@ class HistoryListActivity : BaseComposeActivity(), KoinComponent { screenState = screenState, onEvent = viewModel::handleEvent, pagingDataFlow = viewModel.pagingData, - viewActions = viewModel.viewActions, - onActivityAction = { onCommonActivityAction(it) } + viewActions = viewModel.viewActions ) }, detail = { DetailContent( app = screenState.selectedApp, onDismissRequest = { viewModel.handleEvent(HistoryListEvent.SelectApp(app = null)) }, - onCommonActivityAction = { onCommonActivityAction(it) } ) } ) @@ -74,13 +71,11 @@ class HistoryListActivity : BaseComposeActivity(), KoinComponent { 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) } ) } } 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..e4371261 100644 --- a/app/src/main/java/com/anod/appwatcher/history/HistoryListScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/history/HistoryListScreen.kt @@ -44,7 +44,6 @@ 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 kotlinx.coroutines.flow.Flow import org.koin.java.KoinJavaComponent @@ -54,7 +53,7 @@ fun HistoryListScreen( pagingDataFlow: Flow>, onEvent: (HistoryListEvent) -> Unit, viewActions: Flow, - onActivityAction: (CommonActivityAction) -> Unit, + navigateBack: () -> Unit = {}, appIconLoader: AppIconLoader = KoinJavaComponent.getKoin().get(), ) { val context = LocalContext.current @@ -114,7 +113,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 +122,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..b94b4002 100644 --- a/app/src/main/java/com/anod/appwatcher/history/HistoryListViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/history/HistoryListViewModel.kt @@ -22,7 +22,6 @@ 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 @@ -43,8 +42,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 { @@ -114,7 +113,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 index 29ebaecd..5abdee4e 100644 --- a/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.viewModels import androidx.annotation.Keep import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -18,11 +19,12 @@ 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.ScreenCommonAction +import com.anod.appwatcher.utils.onScreenCommonAction 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 @@ -63,7 +65,6 @@ class InstalledActivity : BaseComposeActivity() { DetailContent( app = screenState.selectedApp, onDismissRequest = { viewModel.handleEvent(InstalledListEvent.SelectApp(app = null)) }, - onCommonActivityAction = { onCommonActivityAction(it) } ) } ) @@ -78,7 +79,6 @@ class InstalledActivity : BaseComposeActivity() { DetailsDialog( app = screenState.selectedApp!!, onDismissRequest = { viewModel.handleEvent(InstalledListEvent.SelectApp(app = null)) }, - onCommonActivityAction = { onCommonActivityAction(it) } ) } } @@ -86,7 +86,7 @@ class InstalledActivity : BaseComposeActivity() { } lifecycleScope.launch { - viewModel.viewActions.collect { onCommonActivityAction(it) } + viewModel.viewActions.collect { onScreenCommonAction(it, { finish() } ) } } lifecycleScope.launch { 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..6a7d8caa 100644 --- a/app/src/main/java/com/anod/appwatcher/installed/InstalledListViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/installed/InstalledListViewModel.kt @@ -14,6 +14,7 @@ import com.anod.appwatcher.accounts.toAndroidAccount import com.anod.appwatcher.database.entities.App import com.anod.appwatcher.utils.BaseFlowViewModel import com.anod.appwatcher.utils.PackageChangedReceiver +import com.anod.appwatcher.utils.ScreenCommonAction import com.anod.appwatcher.utils.SelectionState import com.anod.appwatcher.utils.filterWithExtra import com.anod.appwatcher.utils.getInt @@ -21,7 +22,6 @@ 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.getInstalledPackagesCodes import kotlinx.coroutines.Dispatchers @@ -58,7 +58,7 @@ sealed interface InstalledListEvent { object NoAccount : InstalledListEvent } -class InstalledListViewModel(state: SavedStateHandle) : BaseFlowViewModel(), KoinComponent { +class InstalledListViewModel(state: SavedStateHandle) : BaseFlowViewModel(), KoinComponent { private val importManager: ImportBulkManager by inject() private val packageManager: PackageManager by inject() private val packageChanged: PackageChangedReceiver by inject() @@ -108,7 +108,7 @@ 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() } @@ -121,14 +121,14 @@ class InstalledListViewModel(state: SavedStateHandle) : BaseFlowViewModel AppLog.d("Action collected $action") - handleUiAction(action) + handleUiAction(action, this@SettingsActivity) } } } @@ -123,7 +124,7 @@ open class SettingsActivity : BaseComposeActivity(), GDriveSignIn.Listener { viewModel.handleEvent(SettingsViewEvent.CheckNotificationPermission) } - private fun handleUiAction(action: SettingsViewAction) { + private fun handleUiAction(action: SettingsViewAction, activity: SettingsActivity) { when (action) { is SettingsViewAction.ExportResult -> onExportResult(action.result) is SettingsViewAction.ImportResult -> onImportResult(action.result) @@ -141,7 +142,9 @@ open class SettingsActivity : BaseComposeActivity(), GDriveSignIn.Listener { ProcessPhoenix.triggerRebirth(applicationContext, Intent(applicationContext, AppWatcherActivity::class.java)) } SettingsViewAction.RequestNotificationPermission -> notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) - is SettingsViewAction.ActivityAction -> onCommonActivityAction(action = action.action) + SettingsViewAction.OnBackPressed -> activity.finish() + is SettingsViewAction.ShowToast -> activity.showToast(action) + is SettingsViewAction.StartActivity -> activity.startActivity(action) } } 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..ad4824c0 100644 --- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt @@ -30,7 +30,8 @@ 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 @@ -79,7 +80,9 @@ sealed interface SettingsViewEvent { } sealed interface SettingsViewAction { - class ActivityAction(val action: CommonActivityAction) : SettingsViewAction + data object OnBackPressed : 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 object GDriveSignIn : SettingsViewAction object GDriveSignOut : SettingsViewAction class GDriveErrorIntent(val intent: Intent) : SettingsViewAction @@ -90,24 +93,18 @@ sealed interface SettingsViewAction { class ImportResult(val result: Int) : 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 startActivityAction(intent: Intent): SettingsViewAction { + return SettingsViewAction.StartActivity( + intent = intent, ) } -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 ) } @@ -147,18 +144,15 @@ class SettingsViewModel : BaseFlowViewModel onGDriveLoginResult(event.isSuccess, event.errorCode) SettingsViewEvent.GDriveSyncNow -> gDriveSyncNow() is SettingsViewEvent.GDriveSyncToggle -> gDriveSyncToggle(event.checked) - SettingsViewEvent.OnBackNav -> emitAction(finishAction) + SettingsViewEvent.OnBackNav -> emitAction(SettingsViewAction.OnBackPressed) SettingsViewEvent.OpenRefreshHistory -> emitAction(startActivityAction( Intent(application, SchedulesHistoryActivity::class.java), - addMultiWindowFlags = true )) SettingsViewEvent.OpenUserLog -> emitAction(startActivityAction( Intent(application, UserLogActivity::class.java), - addMultiWindowFlags = true )) SettingsViewEvent.OssLicenses -> emitAction(startActivityAction( Intent(application, OssLicensesMenuActivity::class.java), - addMultiWindowFlags = true )) is SettingsViewEvent.SetRecreateFlag -> { val result = setRecreateFlag(event.item, event.enabled) @@ -181,7 +175,6 @@ class SettingsViewModel : BaseFlowViewModel emitAction(startActivityAction( intent = Intent().forAppInfo(application.packageName), - addMultiWindowFlags = true )) SettingsViewEvent.CheckNotificationPermission -> { val areNotificationsEnabled = prefs.areNotificationsEnabled diff --git a/app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt index 8930be33..d0414530 100644 --- a/app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt @@ -15,7 +15,6 @@ 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() { @@ -43,35 +42,31 @@ open class SearchComposeActivity : BaseComposeActivity() { main = { SearchResultsScreen( screenState = screenState, - onEvent = viewModel::handleEvent, pagingDataFlow = { viewModel.pagingData }, + onEvent = viewModel::handleEvent, viewActions = viewModel.viewActions, - onActivityAction = { onCommonActivityAction(it) }, - onShowAccountDialog = { accountSelectionDialog.show() } + 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 }, + onEvent = viewModel::handleEvent, viewActions = viewModel.viewActions, - onActivityAction = { onCommonActivityAction(it) }, - onShowAccountDialog = { accountSelectionDialog.show() } + onShowAccountDialog = { accountSelectionDialog.show() }, ) if (screenState.selectedApp != null) { DetailsDialog( app = screenState.selectedApp!!, onDismissRequest = { viewModel.handleEvent(SearchViewEvent.SelectApp(app = null)) }, - onCommonActivityAction = { onCommonActivityAction(it) } ) } } 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 0de3661c..dcad955c 100644 --- a/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt @@ -35,7 +35,6 @@ 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.NavBackStack import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems @@ -55,25 +54,25 @@ import finsky.protos.AppDetails import finsky.protos.DocDetails import finsky.protos.DocV2 import info.anodsplace.framework.app.FoldableDeviceLayout -import info.anodsplace.framework.content.CommonActivityAction 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 @Composable -fun SearchResultsScreenScene(wideLayout: FoldableDeviceLayout, backStack: NavBackStack, onCommonActivityAction: (CommonActivityAction) -> Unit) { +fun SearchResultsScreenScene(wideLayout: FoldableDeviceLayout, navigateBack: () -> Unit = {}) { val viewModel: SearchViewModel = viewModel(factory = SearchViewModel.Factory( initialState = SearchViewState(wideLayout = wideLayout), // intentToState(intent, foldableDevice.layout.value), )) val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState) SearchResultsScreen( screenState = screenState, - onEvent = viewModel::handleEvent, pagingDataFlow = { viewModel.pagingData }, + onEvent = viewModel::handleEvent, viewActions = viewModel.viewActions, - onActivityAction = { onCommonActivityAction(it) }, - onShowAccountDialog = { /* accountSelectionDialog.show() */ } + onShowAccountDialog = { /* accountSelectionDialog.show() */ }, + navigateBack = navigateBack ) } @@ -83,8 +82,8 @@ fun SearchResultsScreen( pagingDataFlow: () -> Flow>, onEvent: (SearchViewEvent) -> Unit, viewActions: Flow, - onActivityAction: (CommonActivityAction) -> Unit = { }, onShowAccountDialog: () -> Unit = { }, + navigateBack: () -> Unit = {}, appIconLoader: AppIconLoader = KoinJavaComponent.getKoin().get(), ) { val context = LocalContext.current @@ -147,7 +146,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 = viewActions, key2 = onShowAccountDialog) { viewActions.collect { action -> when (action) { SearchViewAction.ShowAccountDialog -> onShowAccountDialog() @@ -156,36 +155,37 @@ fun SearchResultsScreen( message = action.message, duration = action.duration ) - if (action.exit) { - 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() } } ) @@ -282,8 +282,8 @@ private fun LoadingStatePreview() { screenState = SearchViewState(searchStatus = SearchStatus.Loading), pagingDataFlow = { flowOf() }, onEvent = { }, + viewActions = flowOf(), appIconLoader = appIconLoader, - viewActions = flowOf() ) } } @@ -300,8 +300,8 @@ private fun EmptyStatePreview() { screenState = SearchViewState(searchStatus = SearchStatus.NoResults(query = "")), pagingDataFlow = { flowOf() }, onEvent = { }, + viewActions = flowOf(), appIconLoader = appIconLoader, - viewActions = flowOf() ) } } @@ -318,8 +318,8 @@ private fun RetryStatePreview() { screenState = SearchViewState(searchStatus = SearchStatus.Error("")), pagingDataFlow = { flowOf() }, onEvent = { }, + viewActions = flowOf(), appIconLoader = appIconLoader, - viewActions = flowOf() ) } } @@ -360,8 +360,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 08e6c1d4..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,20 +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 exit: 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 OnBackPressed: 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, ) } @@ -147,7 +144,7 @@ class SearchViewModel( is SearchViewEvent.OnSearchEnter -> onSearchRequest(event.query) is SearchViewEvent.AccountSelectError -> onAccountSelectError(event.errorMessage) is SearchViewEvent.AccountSelected -> onAccountSelected(event.account) - SearchViewEvent.OnBackPressed -> emitAction(SearchViewAction.OnBackPressed) + SearchViewEvent.OnBackPressed -> emitAction(SearchViewAction.NavigateBack) is SearchViewEvent.SelectApp -> { viewState = viewState.copy(selectedApp = event.app) } @@ -157,12 +154,12 @@ class SearchViewModel( private fun onAccountSelectError(errorMessage: String) { if (networkConnection.isNetworkAvailable) { if (errorMessage.isNotBlank()) { - emitAction(SearchViewAction.ShowSnackbar(message = errorMessage, duration = SnackbarDuration.Short, exit = 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, exit = 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, exit = true)) + emitAction(SearchViewAction.ShowSnackbar(message = context.getString(R.string.check_connection), duration = SnackbarDuration.Short, exitScreen = true)) } } @@ -177,7 +174,7 @@ class SearchViewModel( SearchViewAction.ShowSnackbar( message = context.getString(R.string.check_connection), duration = SnackbarDuration.Short, - exit = false + exitScreen = false ) ) } else if (searchStatus is SearchStatus.Error) { @@ -185,7 +182,7 @@ class SearchViewModel( SearchViewAction.ShowSnackbar( message = context.getString(R.string.error_fetching_info), duration = SnackbarDuration.Short, - exit = false + exitScreen = false ) ) } @@ -286,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, exit = 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, exit = 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/SchedulesHistoryActivity.kt index c8f0a700..f23e6319 100644 --- a/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryActivity.kt @@ -35,8 +35,6 @@ 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 java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date @@ -62,7 +60,7 @@ class SchedulesHistoryActivity : BaseComposeActivity(), KoinComponent { SchedulesHistoryScreen( schedules = schedules.toPersistentList(), dateFormat = dateFormat, - onActivityAction = { onCommonActivityAction(it) } + navigateBack = { finish() } ) } } @@ -70,13 +68,13 @@ class SchedulesHistoryActivity : BaseComposeActivity(), KoinComponent { @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/tags/TagWatchListComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt index 7b19e1b8..c2f85211 100644 --- a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt @@ -17,16 +17,15 @@ 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.WatchListAction 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 info.anodsplace.framework.content.showToast +import info.anodsplace.framework.content.startActivity import kotlinx.coroutines.launch class TagWatchListComposeActivity : BaseComposeActivity() { @@ -112,8 +111,9 @@ class TagWatchListComposeActivity : BaseComposeActivity() { lifecycleScope.launch { viewModel.viewActions.collect { when (it) { - is WatchListAction.ActivityAction -> onCommonActivityAction(it.action) is WatchListAction.SelectApp -> {} + is WatchListAction.ShowToast -> showToast(it) + is WatchListAction.StartActivity -> startActivity(it) } } 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..e59c78cb 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 @@ -13,8 +12,8 @@ import com.anod.appwatcher.database.AppTagsTable import com.anod.appwatcher.database.AppsDatabase import com.anod.appwatcher.database.entities.Tag import com.anod.appwatcher.utils.BaseFlowViewModel +import com.anod.appwatcher.utils.ScreenCommonAction import info.anodsplace.compose.CheckBoxItem -import info.anodsplace.framework.content.CommonActivityAction 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 index 349b9b7f..dc80a439 100644 --- a/app/src/main/java/com/anod/appwatcher/userLog/UserLogActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/userLog/UserLogActivity.kt @@ -7,7 +7,7 @@ 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 com.anod.appwatcher.utils.onScreenCommonAction import kotlinx.coroutines.launch /** @@ -30,7 +30,7 @@ class UserLogActivity : BaseComposeActivity() { } lifecycleScope.launch { - viewModel.viewActions.collect { onCommonActivityAction(it) } + viewModel.viewActions.collect { onScreenCommonAction(action = it, navigateBack = { finish() }) } } } } \ 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..eef8d124 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 com.anod.appwatcher.utils.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/ScreenCommonAction.kt b/app/src/main/java/com/anod/appwatcher/utils/ScreenCommonAction.kt new file mode 100644 index 00000000..4a66e225 --- /dev/null +++ b/app/src/main/java/com/anod/appwatcher/utils/ScreenCommonAction.kt @@ -0,0 +1,24 @@ +package com.anod.appwatcher.utils + +import android.content.Context +import android.content.Intent +import android.widget.Toast +import androidx.annotation.StringRes +import info.anodsplace.framework.content.ShowToastActionDefaults +import info.anodsplace.framework.content.StartActivityAction +import info.anodsplace.framework.content.showToast +import info.anodsplace.framework.content.startActivity + +sealed interface ScreenCommonAction { + data class StartActivity(override val intent: Intent) : ScreenCommonAction, StartActivityAction + class ShowToast(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT) : ShowToastActionDefaults(resId, text, length), ScreenCommonAction + object NavigateBack : ScreenCommonAction +} + +fun Context.onScreenCommonAction(action: ScreenCommonAction, navigateBack: () -> Unit) { + when (action) { + ScreenCommonAction.NavigateBack -> navigateBack() + is ScreenCommonAction.ShowToast -> showToast(action) + is ScreenCommonAction.StartActivity -> startActivity(action) + } +} \ 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 ff35f20f..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,10 +11,9 @@ 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) { EmptyBoxSmile() @@ -22,7 +21,6 @@ fun DetailContent(app: App?, onDismissRequest: () -> Unit, onCommonActivityActio DetailsPanel( app = app, onDismissRequest = onDismissRequest, - onCommonActivityAction = onCommonActivityAction ) } } diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt index de379b44..ae7d09a4 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt @@ -26,7 +26,6 @@ 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 @@ -34,7 +33,8 @@ 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.framework.content.showToast +import info.anodsplace.framework.content.startActivity import info.anodsplace.permissions.AppPermission import info.anodsplace.permissions.AppPermissions import info.anodsplace.permissions.toRequestInput @@ -222,8 +222,9 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { 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 -> { } + is MainViewAction.ShowToast -> showToast(action) + is MainViewAction.StartActivity -> startActivity(action) } } 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 636d6248..2967bbc7 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt @@ -1,5 +1,6 @@ package com.anod.appwatcher.watchlist +import android.content.Context import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.material3.DropdownMenuItem @@ -11,11 +12,11 @@ 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.viewmodel.compose.viewModel import androidx.navigation3.runtime.NavBackStack import com.anod.appwatcher.R -import com.anod.appwatcher.compose.AppTheme import com.anod.appwatcher.compose.FilterMenuAction import com.anod.appwatcher.compose.OpenDrawerIcon import com.anod.appwatcher.compose.PlayStoreMyAppsIcon @@ -32,11 +33,12 @@ import com.anod.appwatcher.navigation.WishListNavKey import com.anod.appwatcher.preferences.Preferences import com.anod.appwatcher.tags.EditTagDialog import info.anodsplace.framework.app.FoldableDeviceLayout -import info.anodsplace.framework.content.CommonActivityAction import info.anodsplace.framework.content.InstalledApps +import info.anodsplace.framework.content.showToast +import info.anodsplace.framework.content.startActivity @Composable -fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backStack: NavBackStack, onCommonActivityAction: (CommonActivityAction) -> Unit) { +fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backStack: NavBackStack) { val mainViewModel: MainViewModel = viewModel() val listViewModel: WatchListStateViewModel = viewModel(factory = WatchListStateViewModel.Factory( @@ -50,6 +52,7 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backSt 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 LaunchedEffect(true) { mainViewModel.viewActions.collect { action -> if (action is MainViewAction.DrawerState) { @@ -59,14 +62,15 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backSt drawerState.close() } } else { - onMainAction(action, backStack, onCommonActivityAction) + onMainAction(action, context, backStack) } } } LaunchedEffect(true) { listViewModel.viewActions.collect { action -> when (action) { - is WatchListAction.ActivityAction -> { onCommonActivityAction(action.action) } + is WatchListAction.StartActivity -> context.startActivity(action) + is WatchListAction.ShowToast -> context.showToast(action) is WatchListAction.SelectApp -> backStack.add(SelectedAppNavKey(action.app)) } } @@ -89,7 +93,7 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backSt ) } -private fun onMainAction(action: MainViewAction, backStack: NavBackStack, onCommonActivityAction: (CommonActivityAction) -> Unit) { +private fun onMainAction(action: MainViewAction, context: Context, backStack: NavBackStack) { when (action) { is MainViewAction.NavigateTo -> { when (action.id) { @@ -104,8 +108,9 @@ private fun onMainAction(action: MainViewAction, backStack: NavBackStack, onComm is MainViewAction.NavigateToTag -> backStack.add(TagWatchListNavKey(tag = action.tag)) MainViewAction.RequestNotificationPermission -> {} //notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) MainViewAction.ChooseAccount -> {} //accountSelectionDialog.show() - is MainViewAction.ActivityAction -> onCommonActivityAction(action.action) + is MainViewAction.ShowToast -> context.showToast(action) is MainViewAction.DrawerState -> { } + is MainViewAction.StartActivity -> context.startActivity(action) } } @Composable 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..33395ea4 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 @@ -58,7 +59,8 @@ sealed interface MainViewEvent { } sealed interface MainViewAction { - class ActivityAction(val action: CommonActivityAction) : 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 object ChooseAccount : MainViewAction class NavigateTo(val id: DrawerItem.Id) : MainViewAction data object RequestNotificationPermission : MainViewAction @@ -66,24 +68,15 @@ sealed interface 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() diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt index 128ee116..a0e14e2a 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt @@ -11,7 +11,6 @@ import android.graphics.drawable.Icon import android.widget.Toast import androidx.compose.runtime.Immutable import androidx.core.content.ContextCompat -import androidx.lifecycle.AbstractSavedStateViewModelFactory import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -38,7 +37,6 @@ import com.anod.appwatcher.utils.prefs import com.anod.appwatcher.utils.syncProgressFlow import info.anodsplace.applog.AppLog import info.anodsplace.framework.app.FoldableDeviceLayout -import info.anodsplace.framework.content.CommonActivityAction import info.anodsplace.framework.content.InstalledApps import info.anodsplace.framework.content.PinShortcut import info.anodsplace.framework.content.PinShortcutManager @@ -64,6 +62,9 @@ import androidx.core.graphics.drawable.toDrawable import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.CreationExtras +import info.anodsplace.framework.content.ShowToastAction +import info.anodsplace.framework.content.ShowToastActionDefaults +import info.anodsplace.framework.content.StartActivityAction import kotlin.reflect.KClass @Immutable @@ -111,25 +112,21 @@ sealed interface WatchListEvent { } sealed interface WatchListAction { - data class ActivityAction(val action: CommonActivityAction) : WatchListAction + data class StartActivity(override val intent: Intent) : WatchListAction, StartActivityAction + class ShowToast(resId: Int, text: String, length: Int) : ShowToastActionDefaults(resId, text, length), WatchListAction data class SelectApp(val app: App) : WatchListAction } -private fun CommonActivityAction.toWatchListAction(): WatchListAction.ActivityAction = WatchListAction.ActivityAction(this) +private fun startActivityAction(intent: Intent): WatchListAction + = WatchListAction.StartActivity(intent) -private fun startActivityAction(intent: Intent, addMultiWindowFlags: Boolean = false): WatchListAction.ActivityAction - = CommonActivityAction.StartActivity( - intent = intent, - addMultiWindowFlags = addMultiWindowFlags - ).toWatchListAction() - -private fun showToastAction(resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): WatchListAction.ActivityAction - = CommonActivityAction.ShowToast( +private fun showToastAction(resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): WatchListAction + = WatchListAction.ShowToast( resId = resId, text = text, length = length - ).toWatchListAction() + ) class WatchListStateViewModel( state: SavedStateHandle, @@ -204,7 +201,7 @@ class WatchListStateViewModel( .observeTag(viewState.tag.id) .collect { tag -> if (tag == null) { - emitAction(CommonActivityAction.Finish.toWatchListAction()) + // TODO: emitAction(CommonActivityAction.Finish.toWatchListAction()) } else { viewState = viewState.copy( tag = tag, @@ -303,7 +300,6 @@ class WatchListStateViewModel( } WatchListEvent.PlayStoreMyApps -> emitAction(startActivityAction( intent = Intent().forMyApps(true), - addMultiWindowFlags = true )) WatchListEvent.Refresh -> refresh() is WatchListEvent.AppClick -> { diff --git a/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt b/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt index ae728cd6..c415dbf8 100644 --- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt @@ -16,7 +16,6 @@ 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 @@ -55,14 +54,12 @@ class WishListActivity : BaseComposeActivity(), KoinComponent { 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) } ) } ) @@ -72,13 +69,11 @@ class WishListActivity : BaseComposeActivity(), KoinComponent { 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) } ) } } 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..059afc19 100644 --- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/wishlist/WishListScreen.kt @@ -45,7 +45,6 @@ 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 kotlinx.coroutines.flow.Flow import org.koin.java.KoinJavaComponent @@ -55,7 +54,7 @@ fun WishListScreen( pagingDataFlow: Flow>, onEvent: (WishListEvent) -> Unit, viewActions: Flow, - onActivityAction: (CommonActivityAction) -> Unit, + navigateBack: () -> Unit = {}, appIconLoader: AppIconLoader = KoinJavaComponent.getKoin().get(), ) { val context = LocalContext.current @@ -115,7 +114,6 @@ fun WishListScreen( } var showTagList: App? by remember { mutableStateOf(null) } - val latestOnActivityAction by rememberUpdatedState(onActivityAction) LaunchedEffect(key1 = viewActions) { viewActions.collect { action -> when (action) { @@ -125,7 +123,8 @@ fun WishListScreen( showTagList = action.info } } - is WishListAction.ActivityAction -> latestOnActivityAction(action.action) + + is WishListAction.NavigateBack -> navigateBack() } } } 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..74d2dc49 100644 --- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt @@ -22,7 +22,6 @@ 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.AppDetailsFilter import info.anodsplace.playstore.AppNameFilter @@ -42,7 +41,7 @@ data class WishListState( sealed interface WishListAction { class ShowTagSnackbar(val info: App) : WishListAction - class ActivityAction(val action: CommonActivityAction) : WishListAction + data object NavigateBack : WishListAction } sealed interface WishListEvent { @@ -115,7 +114,7 @@ class WishListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel 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) diff --git a/lib b/lib index 618410c4..0b845e6b 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit 618410c45450f2b00e51834ee502b8a9239ecdab +Subproject commit 0b845e6b87d33e4c2b18cb15c1a4640ae27c836e From ae1ed89de184ee94b6afa0ae5531691b64a73963 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Sat, 21 Jun 2025 13:19:30 +0300 Subject: [PATCH 05/15] Settings screen migration --- .../com/anod/appwatcher/AppWatcherActivity.kt | 9 +- .../appwatcher/backup/gdrive/GDriveSignIn.kt | 59 +++++------- .../preferences/SettingsActivity.kt | 23 +---- .../appwatcher/preferences/SettingsScreen.kt | 91 +++++++++++++++++++ .../preferences/SettingsViewModel.kt | 41 +++++++-- 5 files changed, 163 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt index f56ffb9e..b5632a7e 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -28,6 +28,8 @@ import com.anod.appwatcher.compose.AppTheme import com.anod.appwatcher.navigation.MainScreenNavKey import com.anod.appwatcher.navigation.MarketSearchNavKey import com.anod.appwatcher.navigation.SelectedAppNavKey +import com.anod.appwatcher.navigation.SettingsNavKey +import com.anod.appwatcher.preferences.SettingsScreenScene import com.anod.appwatcher.search.SearchResultsScreenScene @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -75,7 +77,7 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { ) { key -> DetailContent( app = key.selectedApp, - onDismissRequest = { }, + onDismissRequest = { backStack.removeLastOrNull() }, ) } entry( @@ -92,6 +94,11 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { navigateBack = { backStack.removeLastOrNull() }, ) } + entry { + SettingsScreenScene( + navigateBack = { backStack.removeLastOrNull() } + ) + } } companion object { 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..82716def 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 @@ -28,6 +28,7 @@ import java.util.concurrent.ExecutionException import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import org.koin.java.KoinJavaComponent +import kotlin.coroutines.resumeWithException internal fun createGDriveSignInOptions(): GoogleSignInOptions { return GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) @@ -42,13 +43,9 @@ internal fun createCredentials(context: Context, googleAccount: GoogleSignInAcco .setSelectedAccount(googleAccount.account) } -interface ResultListener { - fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) -} - -class GDriveSignIn(private val activity: Activity, private val listener: Listener) : ResultListener { +class GDriveSignIn(private val context: ApplicationContext) { - 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 @@ -67,25 +64,23 @@ class GDriveSignIn(private val activity: Activity, private val listener: Listene } } - interface Listener { - fun onGDriveLoginSuccess(googleSignInAccount: GoogleSignInAccount) - fun onGDriveLoginError(errorCode: Int) - } + class GoogleSignInRequestException(val intent: Intent, val resultCode: Int) : Throwable() + class GoogleSignInFailedException(val resultCode: Int) : Throwable() - fun signIn() { + suspend fun signIn() = suspendCoroutine { continuation -> driveConnect.connect(object : GoogleSignInConnect.Result { override fun onSuccess(account: GoogleSignInAccount, client: GoogleSignInClient) { - listener.onGDriveLoginSuccess(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) + continuation.resumeWithException(GoogleSignInRequestException(client.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) @@ -94,34 +89,30 @@ 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")) + // GoogleSignIn.requestPermissions(activity, RESULT_CODE_GDRIVE_SIGN_IN, lastSignedAccount, Scope("email")) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + suspend fun onActivityResult(requestCode: Int, 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) { diff --git a/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt b/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt index ab9bd98b..9f843a2e 100644 --- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt @@ -25,12 +25,10 @@ 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.showToast @@ -41,11 +39,10 @@ import info.anodsplace.permissions.toRequestInput import kotlinx.coroutines.launch @SuppressLint("Registered") -open class SettingsActivity : BaseComposeActivity(), GDriveSignIn.Listener { +open class SettingsActivity : BaseComposeActivity() { 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?) { @@ -53,7 +50,9 @@ open class SettingsActivity : BaseComposeActivity(), GDriveSignIn.Listener { viewModel.handleEvent(SettingsViewEvent.SetWideLayout(foldableDevice.layout.value)) - gDriveErrorIntentRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { } + gDriveErrorIntentRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + viewModel.handleEvent(SettingsViewEvent.GDriveActivityResult(activityResult)) + } notificationPermissionRequest = registerForActivityResult(AppPermissions.Request()) { val enabled = it[AppPermission.PostNotification.value] ?: false viewModel.handleEvent(SettingsViewEvent.NotificationPermissionResult(enabled)) @@ -128,10 +127,6 @@ open class SettingsActivity : BaseComposeActivity(), GDriveSignIn.Listener { 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)) @@ -182,7 +177,7 @@ open class SettingsActivity : BaseComposeActivity(), GDriveSignIn.Listener { @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - gDriveSignIn.onActivityResult(requestCode, resultCode, data) + //gDriveSignIn.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data) } @@ -191,12 +186,4 @@ open class SettingsActivity : BaseComposeActivity(), GDriveSignIn.Listener { 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..6220552a 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.lifecycleScope +import androidx.lifecycle.viewmodel.compose.viewModel +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.backup.gdrive.GDriveSignIn 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) { + 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.OnBackPressed -> navigateBack() + 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()) { 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 ad4824c0..70677e2e 100644 --- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt @@ -5,6 +5,7 @@ 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 @@ -13,6 +14,7 @@ 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 @@ -61,6 +63,7 @@ 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 @@ -70,7 +73,6 @@ sealed interface 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 @@ -83,8 +85,6 @@ sealed interface SettingsViewAction { data object OnBackPressed : 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 - object GDriveSignIn : SettingsViewAction - object GDriveSignOut : SettingsViewAction class GDriveErrorIntent(val intent: Intent) : SettingsViewAction object Recreate : SettingsViewAction object Rebirth : SettingsViewAction @@ -93,7 +93,6 @@ sealed interface SettingsViewAction { class ImportResult(val result: Int) : SettingsViewAction } - private fun startActivityAction(intent: Intent): SettingsViewAction { return SettingsViewAction.StartActivity( intent = intent, @@ -114,6 +113,7 @@ class SettingsViewModel : BaseFlowViewModel onGDriveLoginResult(event.isSuccess, event.errorCode) SettingsViewEvent.GDriveSyncNow -> gDriveSyncNow() is SettingsViewEvent.GDriveSyncToggle -> gDriveSyncToggle(event.checked) SettingsViewEvent.OnBackNav -> emitAction(SettingsViewAction.OnBackPressed) @@ -195,6 +194,7 @@ class SettingsViewModel : BaseFlowViewModel { viewState = viewState.copy(wideLayout = event.wideLayout) } + is SettingsViewEvent.GDriveActivityResult -> onGDriveActivityResult(event.activityResult) } } @@ -217,14 +217,41 @@ 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.resultCode, activityResult.data) + onGDriveLoginResult(true, -1) + } catch (e: Throwable) { + when (e) { + is GDriveSignIn.GoogleSignInFailedException -> onGDriveLoginResult(false, e.resultCode) + else -> onGDriveLoginResult(false, 0) + } + } } } From 85443fe58cb586d61746d7fcc897b58342872f5a Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Sat, 21 Jun 2025 16:41:48 +0300 Subject: [PATCH 06/15] TagWatchListScreenScene --- .../com/anod/appwatcher/AppWatcherActivity.kt | 25 +++++- .../anod/appwatcher/compose/SearchTopBar.kt | 1 - .../com/anod/appwatcher/navigation/NavKey.kt | 6 +- .../appwatcher/search/SearchResultsScreen.kt | 2 +- .../tags/TagWatchListComposeActivity.kt | 2 + .../appwatcher/tags/TagWatchListScreen.kt | 83 +++++++++++++++++++ .../anod/appwatcher/watchlist/MainActivity.kt | 2 +- .../anod/appwatcher/watchlist/MainScreen.kt | 29 ++++--- .../watchlist/WatchListStateViewModel.kt | 56 +++++-------- .../appwatcher/watchlist/WatchListTopBar.kt | 2 +- 10 files changed, 155 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt index b5632a7e..81d26ccb 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -29,8 +29,10 @@ import com.anod.appwatcher.navigation.MainScreenNavKey import com.anod.appwatcher.navigation.MarketSearchNavKey import com.anod.appwatcher.navigation.SelectedAppNavKey import com.anod.appwatcher.navigation.SettingsNavKey +import com.anod.appwatcher.navigation.TagWatchListNavKey import com.anod.appwatcher.preferences.SettingsScreenScene import com.anod.appwatcher.search.SearchResultsScreenScene +import com.anod.appwatcher.tags.TagWatchListScreenScene @OptIn(ExperimentalMaterial3AdaptiveApi::class) class AppWatcherActivity : BaseComposeActivity(), KoinComponent { @@ -69,7 +71,8 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { MainScreenScene( prefs = prefs, wideLayout = wideLayout, - backStack = backStack, + navigateBack = { backStack.removeLastOrNull() }, + navigateTo = { backStack.add(it) } ) } entry( @@ -94,11 +97,29 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { navigateBack = { backStack.removeLastOrNull() }, ) } - entry { + entry( + metadata = ListDetailSceneStrategy.extraPane(sceneKey = SettingsNavKey) + ) { SettingsScreenScene( navigateBack = { backStack.removeLastOrNull() } ) } + entry( + metadata = ListDetailSceneStrategy.listPane( + sceneKey = TagWatchListNavKey, + detailPlaceholder = { + EmptyBoxSmile() + } + ) + ) { key -> + val wideLayout by foldableDevice.layout.collectAsState() + TagWatchListScreenScene( + tag = key.tag, + wideLayout = wideLayout, + navigateBack = { backStack.removeLastOrNull() }, + navigateTo = { backStack.add(it) } + ) + } } companion object { 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/navigation/NavKey.kt b/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt index 0b437918..7e6fa52f 100644 --- a/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt +++ b/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt @@ -9,7 +9,11 @@ import kotlinx.serialization.Serializable data object MainScreenNavKey : NavKey @Serializable -data object MarketSearchNavKey : NavKey +data class MarketSearchNavKey( + val keyword: String = "", + val focus: Boolean = false, + val initiateSearch: Boolean = false +) : NavKey @Serializable data object SettingsNavKey : NavKey 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 dcad955c..621ab6ee 100644 --- a/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt @@ -146,7 +146,7 @@ fun SearchResultsScreen( var showTagList: Pair? by remember { mutableStateOf(null) } var deleteNoticeDocument: Document? by remember { mutableStateOf(null) } - LaunchedEffect(key1 = viewActions, key2 = onShowAccountDialog) { + LaunchedEffect(key1 = true, key2 = onShowAccountDialog) { viewActions.collect { action -> when (action) { SearchViewAction.ShowAccountDialog -> onShowAccountDialog() diff --git a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt index c2f85211..79fe6cdd 100644 --- a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt @@ -114,6 +114,8 @@ class TagWatchListComposeActivity : BaseComposeActivity() { is WatchListAction.SelectApp -> {} is WatchListAction.ShowToast -> showToast(it) is WatchListAction.StartActivity -> startActivity(it) + WatchListAction.NavigateBack -> finish() + is WatchListAction.NavigateTo -> {} } } 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..33f7a191 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,102 @@ 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.core.os.bundleOf +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.TagWatchListNavKey +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 = TagWatchListNavKey.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/watchlist/MainActivity.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt index ae7d09a4..6c8c0182 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt @@ -92,7 +92,7 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { if (mainViewModel.viewState.isDrawerOpen) { mainViewModel.handleEvent(MainViewEvent.DrawerState(isOpen = false)) } else { - listViewModel.handleEvent(WatchListEvent.NavigationButton) + listViewModel.handleEvent(WatchListEvent.OnBackPressed) } } }) 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 2967bbc7..b56d3c53 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey import com.anod.appwatcher.R import com.anod.appwatcher.compose.FilterMenuAction import com.anod.appwatcher.compose.OpenDrawerIcon @@ -25,6 +26,7 @@ import com.anod.appwatcher.compose.SortMenuItem import com.anod.appwatcher.database.entities.Tag import com.anod.appwatcher.navigation.HistoryNavKey import com.anod.appwatcher.navigation.InstalledNavKey +import com.anod.appwatcher.navigation.MainScreenNavKey import com.anod.appwatcher.navigation.MarketSearchNavKey import com.anod.appwatcher.navigation.SelectedAppNavKey import com.anod.appwatcher.navigation.SettingsNavKey @@ -38,14 +40,15 @@ import info.anodsplace.framework.content.showToast import info.anodsplace.framework.content.startActivity @Composable -fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backStack: NavBackStack) { +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 - ) + ), + key = MainScreenNavKey.toString() ) val mainState by mainViewModel.viewStates.collectAsState(initial = mainViewModel.viewState) @@ -62,7 +65,7 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backSt drawerState.close() } } else { - onMainAction(action, context, backStack) + onMainAction(action, context, navigateTo) } } } @@ -71,7 +74,9 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backSt when (action) { is WatchListAction.StartActivity -> context.startActivity(action) is WatchListAction.ShowToast -> context.showToast(action) - is WatchListAction.SelectApp -> backStack.add(SelectedAppNavKey(action.app)) + is WatchListAction.SelectApp -> navigateTo(SelectedAppNavKey(action.app)) + WatchListAction.NavigateBack -> navigateBack() + is WatchListAction.NavigateTo -> navigateTo(action.navKey) } } } @@ -93,19 +98,19 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, backSt ) } -private fun onMainAction(action: MainViewAction, context: Context, backStack: NavBackStack) { +private fun onMainAction(action: MainViewAction, context: Context, navigateTo: (NavKey) -> Unit) { when (action) { is MainViewAction.NavigateTo -> { when (action.id) { - DrawerItem.Id.Add -> backStack.add(MarketSearchNavKey) - DrawerItem.Id.Installed -> backStack.add(InstalledNavKey(importMode = false)) + DrawerItem.Id.Add -> navigateTo(MarketSearchNavKey()) + DrawerItem.Id.Installed -> navigateTo(InstalledNavKey(importMode = false)) DrawerItem.Id.Refresh -> {} - DrawerItem.Id.Settings -> backStack.add(SettingsNavKey) - DrawerItem.Id.Wishlist -> backStack.add(WishListNavKey) - DrawerItem.Id.Purchases -> backStack.add(HistoryNavKey) + DrawerItem.Id.Settings -> navigateTo(SettingsNavKey) + DrawerItem.Id.Wishlist -> navigateTo(WishListNavKey) + DrawerItem.Id.Purchases -> navigateTo(HistoryNavKey) } } - is MainViewAction.NavigateToTag -> backStack.add(TagWatchListNavKey(tag = action.tag)) + is MainViewAction.NavigateToTag -> navigateTo(TagWatchListNavKey(tag = action.tag)) MainViewAction.RequestNotificationPermission -> {} //notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) MainViewAction.ChooseAccount -> {} //accountSelectionDialog.show() is MainViewAction.ShowToast -> context.showToast(action) @@ -152,7 +157,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/WatchListStateViewModel.kt b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt index a0e14e2a..ec91c09f 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt @@ -8,6 +8,7 @@ import android.graphics.Rect import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.Icon +import android.os.Bundle import android.widget.Toast import androidx.compose.runtime.Immutable import androidx.core.content.ContextCompat @@ -59,10 +60,13 @@ import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import androidx.core.graphics.drawable.toDrawable +import androidx.lifecycle.DEFAULT_ARGS_KEY import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.CreationExtras -import info.anodsplace.framework.content.ShowToastAction +import androidx.navigation3.runtime.NavKey +import com.anod.appwatcher.navigation.InstalledNavKey +import com.anod.appwatcher.navigation.MarketSearchNavKey import info.anodsplace.framework.content.ShowToastActionDefaults import info.anodsplace.framework.content.StartActivityAction import kotlin.reflect.KClass @@ -88,7 +92,7 @@ data class WatchListSharedState( ) sealed interface WatchListEvent { - data object NavigationButton : WatchListEvent + data object OnBackPressed : WatchListEvent data object PlayStoreMyApps : WatchListEvent data object Refresh : WatchListEvent @@ -115,12 +119,13 @@ sealed interface WatchListAction { data class StartActivity(override val intent: Intent) : WatchListAction, StartActivityAction class ShowToast(resId: Int, text: String, length: Int) : ShowToastActionDefaults(resId, text, length), WatchListAction data class SelectApp(val app: App) : WatchListAction + class NavigateTo(val navKey: NavKey): WatchListAction + data object NavigateBack : WatchListAction } private fun startActivityAction(intent: Intent): WatchListAction = WatchListAction.StartActivity(intent) - private fun showToastAction(resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): WatchListAction = WatchListAction.ShowToast( resId = resId, @@ -130,6 +135,7 @@ private fun showToastAction(resId: Int = 0, text: String = "", length: Int = Toa class WatchListStateViewModel( state: SavedStateHandle, + initialTag: Tag, defaultFilterId: Int, collectRecentlyInstalledApps: Boolean, wideLayout: FoldableDeviceLayout @@ -153,12 +159,14 @@ class WatchListStateViewModel( class Factory( private val defaultFilterId: Int, private val wideLayout: FoldableDeviceLayout, - private val collectRecentlyInstalledApps: Boolean + private val collectRecentlyInstalledApps: Boolean, + private val initialTag: Tag = Tag.empty ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: KClass, extras: CreationExtras): T { return WatchListStateViewModel( state = extras.createSavedStateHandle(), + initialTag = initialTag, defaultFilterId = defaultFilterId, wideLayout = wideLayout, collectRecentlyInstalledApps = collectRecentlyInstalledApps @@ -173,7 +181,9 @@ class WatchListStateViewModel( 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 + if (extraTagId != 0) { + Tag(extraTagId, "", state[EXTRA_TAG_COLOR] ?: Tag.DEFAULT_COLOR) + } else initialTag } else { extraTag } @@ -262,30 +272,14 @@ class WatchListStateViewModel( is WatchListEvent.EditTag -> viewState = viewState.copy(showEditTagDialog = event.show) is WatchListEvent.ShowSearch -> viewState = viewState.copy(showSearch = event.show) is WatchListEvent.SelectApp -> emitAction(WatchListAction.SelectApp(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) -// } - } + 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( + MarketSearchNavKey(keyword = query, focus = true) + )) } is WatchListEvent.UpdateSyncProgress -> { @@ -308,16 +302,10 @@ class WatchListStateViewModel( 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( + MarketSearchNavKey(focus = true) )) + 2 -> emitAction(WatchListAction.NavigateTo(InstalledNavKey(importMode = true))) 3 -> emitAction(startActivityAction( intent = Intent.makeMainActivity(ComponentName("com.android.vending", "com.android.vending.AssetBrowserActivity")) )) 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)) From 469b8116bad1e44b008117dca402b9ee95d9cd94 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Fri, 27 Jun 2025 21:48:43 +0300 Subject: [PATCH 07/15] Kotlin 2.2.0, upgrade libraries --- app/build.gradle.kts | 12 ++- .../anod/appwatcher/database/AppsDatabase.kt | 92 +++++++++---------- .../anod/appwatcher/database/entities/Tag.kt | 2 +- .../appwatcher/details/DetailsViewModel.kt | 2 +- .../preferences/SettingsActivity.kt | 6 -- .../anod/appwatcher/tags/EditTagViewModel.kt | 2 +- .../anod/appwatcher/utils/color/ColorRoles.kt | 8 +- .../anod/appwatcher/watchlist/DrawerItem.kt | 2 +- gradle/libs.versions.toml | 24 ++--- gradle/wrapper/gradle-wrapper.properties | 2 +- lib | 2 +- macrobenchmark/build.gradle.kts | 12 ++- playstore/build.gradle.kts | 11 ++- 13 files changed, 92 insertions(+), 85 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fafe196e..92ffaebf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin) @@ -10,6 +12,12 @@ plugins { id("com.google.android.gms.oss-licenses-plugin") } +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } +} + android { compileSdk = 36 @@ -69,10 +77,6 @@ android { targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } - packaging { resources { excludes += "META-INF/DEPENDENCIES" 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/Tag.kt b/app/src/main/java/com/anod/appwatcher/database/entities/Tag.kt index 3d09ae23..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 @@ -26,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/DetailsViewModel.kt b/app/src/main/java/com/anod/appwatcher/details/DetailsViewModel.kt index cdb73163..f51079fd 100644 --- a/app/src/main/java/com/anod/appwatcher/details/DetailsViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/details/DetailsViewModel.kt @@ -118,7 +118,7 @@ sealed interface DetailsAction { class ShowTagSnackbar(val appInfo: App) : DetailsAction object Dismiss : DetailsAction class Share(val app: App, val recentChange: AppChange) : DetailsAction - class ShowToast(@StringRes override val resId: Int) : ShowToastActionDefaults(resId), DetailsAction + class ShowToast(@param:StringRes override val resId: Int) : ShowToastActionDefaults(resId), DetailsAction } private fun startActivityAction(intent: Intent): DetailsAction diff --git a/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt b/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt index 9f843a2e..26a320ad 100644 --- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt @@ -175,12 +175,6 @@ open class SettingsActivity : BaseComposeActivity() { } } - @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 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/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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml index e82ea83e..f37f4cd7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,29 +1,29 @@ [versions] activity-compose = "1.10.1" -agp = "8.10.1" +agp = "8.11.0" annotation = "1.9.1" -ksp = "2.1.21-2.0.1" +ksp = "2.2.0-2.0.2" androidx-junit = "1.2.1" 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" +compose-bom = "2025.06.01" +compose-material3 = "1.4.0-alpha16" 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-analytics = "22.5.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" +google-services = "4.4.3" junit = "4.13.2" koin-core = "4.1.0" -kotlin = "2.1.21" +kotlin = "2.2.0" kotlinx-collections-immutable = "0.4.0" -kotlinx-datetime = "0.6.2" -kotlinx-serialization = "1.8.1" +kotlinx-datetime = "0.7.0" +kotlinx-serialization = "1.9.0" ktor = "3.1.3" ktlint-gradle = "12.3.0" ktlint-compose = "0.4.22" @@ -40,11 +40,11 @@ play-services-auth = "21.3.0" play-services-identity = "18.1.0" play-services-oss-licenses = "17.1.0" process-phoenix = "3.0.0" -room = "2.7.1" -runtime-tracing = "1.8.2" +room = "2.7.2" +runtime-tracing = "1.8.3" uiautomator = "2.3.0" window = "1.4.0" -work-runtime = "2.10.1" +work-runtime = "2.10.2" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8fc91c82..c3f6c760 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ 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-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lib b/lib index 0b845e6b..aeaca8f7 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit 0b845e6b87d33e4c2b18cb15c1a4640ae27c836e +Subproject commit aeaca8f76116d05f78792c5610f1d1d4d21a2c60 diff --git a/macrobenchmark/build.gradle.kts b/macrobenchmark/build.gradle.kts index fa86ede9..1c9a6238 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,10 +20,6 @@ android { targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } - defaultConfig { minSdk = 28 targetSdk = 36 diff --git a/playstore/build.gradle.kts b/playstore/build.gradle.kts index 8f47477c..2c04b87b 100644 --- a/playstore/build.gradle.kts +++ b/playstore/build.gradle.kts @@ -1,8 +1,16 @@ +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 @@ -16,9 +24,6 @@ android { targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } namespace = "info.anodsplace.playstore" } From 4055b761d98d7e497ef3ae80cad4ea6ad0f3ccc0 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Sat, 28 Jun 2025 19:19:31 +0300 Subject: [PATCH 08/15] Migrate InstalledListScreen --- .../com/anod/appwatcher/AppWatcherActivity.kt | 80 +++++++++++++------ .../appwatcher/installed/InstalledActivity.kt | 13 ++- .../installed/InstalledListScreen.kt | 49 ++++++++++++ .../installed/InstalledListViewModel.kt | 29 ++++++- .../com/anod/appwatcher/navigation/NavKey.kt | 34 -------- .../anod/appwatcher/navigation/SceneNavKey.kt | 37 +++++++++ .../appwatcher/tags/TagWatchListScreen.kt | 5 +- .../anod/appwatcher/watchlist/MainActivity.kt | 5 +- .../anod/appwatcher/watchlist/MainScreen.kt | 26 +++--- .../watchlist/WatchListStateViewModel.kt | 31 +++---- 10 files changed, 200 insertions(+), 109 deletions(-) delete mode 100644 app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt create mode 100644 app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt index 81d26ccb..bce86ee5 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -9,30 +9,29 @@ 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.installed.InstalledListScreenScene +import com.anod.appwatcher.navigation.SceneNavKey +import com.anod.appwatcher.preferences.SettingsScreenScene +import com.anod.appwatcher.search.SearchResultsScreenScene +import com.anod.appwatcher.tags.TagWatchListScreenScene 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.watchlist.WatchListStateViewModel import org.koin.core.component.KoinComponent -import androidx.core.net.toUri -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavEntry -import com.anod.appwatcher.compose.AppTheme -import com.anod.appwatcher.navigation.MainScreenNavKey -import com.anod.appwatcher.navigation.MarketSearchNavKey -import com.anod.appwatcher.navigation.SelectedAppNavKey -import com.anod.appwatcher.navigation.SettingsNavKey -import com.anod.appwatcher.navigation.TagWatchListNavKey -import com.anod.appwatcher.preferences.SettingsScreenScene -import com.anod.appwatcher.search.SearchResultsScreenScene -import com.anod.appwatcher.tags.TagWatchListScreenScene @OptIn(ExperimentalMaterial3AdaptiveApi::class) class AppWatcherActivity : BaseComposeActivity(), KoinComponent { @@ -41,8 +40,10 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { setTheme(R.style.AppTheme_Main) super.onCreate(savedInstanceState) + val elements = createInitialBackstack() setContent { - val backStack = rememberNavBackStack(MainScreenNavKey) + + val backStack = rememberNavBackStack(*elements) val listDetailStrategy = rememberListDetailSceneStrategy() AppTheme( theme = prefs.theme, @@ -58,10 +59,30 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { } } + private fun createInitialBackstack(): Array { + val extras = intent?.extras ?: bundleOf() + var elements = arrayOf(SceneNavKey.Main) + if (extras.containsKey("open_recently_installed")) { + intent!!.extras!!.remove("open_recently_installed") + elements += SceneNavKey.Installed(importMode = false) + } else if (extras.containsKey(WatchListStateViewModel.EXTRA_TAG_ID)) { + val extraTagId = extras.getInt(WatchListStateViewModel.EXTRA_TAG_ID) + intent!!.extras!!.remove(WatchListStateViewModel.EXTRA_TAG_ID) + elements += SceneNavKey.TagWatchList( + tag = Tag( + id = extraTagId, + name = "", + color = extras.getInt(WatchListStateViewModel.EXTRA_TAG_COLOR) + ) + ) + } + return elements + } + private fun provideNavEntries(backStack: NavBackStack): (NavKey) -> NavEntry = entryProvider { - entry( + entry( metadata = ListDetailSceneStrategy.listPane( - sceneKey = MainScreenNavKey, + sceneKey = SceneNavKey.Main, detailPlaceholder = { EmptyBoxSmile() } @@ -75,17 +96,17 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { navigateTo = { backStack.add(it) } ) } - entry( - metadata = ListDetailSceneStrategy.detailPane() + entry( + metadata = ListDetailSceneStrategy.detailPane(sceneKey = SceneNavKey.AppDetails) ) { key -> DetailContent( app = key.selectedApp, onDismissRequest = { backStack.removeLastOrNull() }, ) } - entry( + entry( metadata = ListDetailSceneStrategy.listPane( - sceneKey = MarketSearchNavKey, + sceneKey = SceneNavKey.Search, detailPlaceholder = { EmptyBoxSmile() } @@ -97,16 +118,16 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { navigateBack = { backStack.removeLastOrNull() }, ) } - entry( - metadata = ListDetailSceneStrategy.extraPane(sceneKey = SettingsNavKey) + entry( + metadata = ListDetailSceneStrategy.extraPane(sceneKey = SceneNavKey.Settings) ) { SettingsScreenScene( navigateBack = { backStack.removeLastOrNull() } ) } - entry( + entry( metadata = ListDetailSceneStrategy.listPane( - sceneKey = TagWatchListNavKey, + sceneKey = SceneNavKey.TagWatchList, detailPlaceholder = { EmptyBoxSmile() } @@ -120,6 +141,19 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { navigateTo = { backStack.add(it) } ) } + entry( + metadata = ListDetailSceneStrategy.listPane( + sceneKey = SceneNavKey.Installed, + detailPlaceholder = { + EmptyBoxSmile() + } + ) + ) { key -> + InstalledListScreenScene( + showAction = key.importMode, + navigateBack = { backStack.removeLastOrNull() }, + ) + } } companion object { diff --git a/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt b/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt index 5abdee4e..fa45edd3 100644 --- a/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt @@ -9,7 +9,6 @@ import androidx.activity.viewModels import androidx.annotation.Keep import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -18,8 +17,6 @@ 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.ScreenCommonAction import com.anod.appwatcher.utils.onScreenCommonAction import com.anod.appwatcher.utils.prefs import com.anod.appwatcher.watchlist.DetailContent @@ -106,10 +103,10 @@ class InstalledActivity : BaseComposeActivity() { } } - fun intent(importMode: Boolean, context: Context) = intent( - if (importMode) Preferences.SORT_NAME_ASC else Preferences.SORT_DATE_DESC, - importMode, - context - ) +// 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..a99f2b03 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,63 @@ 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.utils.ScreenCommonAction 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.showToast +import info.anodsplace.framework.content.startActivity + +@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 -> + when (action) { + ScreenCommonAction.NavigateBack -> navigateBack() + is ScreenCommonAction.ShowToast -> context.showToast(action) + is ScreenCommonAction.StartActivity -> context.startActivity(action) + } + } + } +} @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 6a7d8caa..7a1f91b1 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 @@ -29,6 +33,7 @@ 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( @@ -58,7 +63,11 @@ sealed interface 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 +75,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 { diff --git a/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt b/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt deleted file mode 100644 index 7e6fa52f..00000000 --- a/app/src/main/java/com/anod/appwatcher/navigation/NavKey.kt +++ /dev/null @@ -1,34 +0,0 @@ -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 - -@Serializable -data object MainScreenNavKey : NavKey - -@Serializable -data class MarketSearchNavKey( - val keyword: String = "", - val focus: Boolean = false, - val initiateSearch: Boolean = false -) : NavKey - -@Serializable -data object SettingsNavKey : NavKey - -@Serializable -data class InstalledNavKey(val importMode: Boolean) : NavKey - -@Serializable -data object WishListNavKey : NavKey - -@Serializable -data object HistoryNavKey : NavKey - -@Serializable -data class TagWatchListNavKey(val tag: Tag) : NavKey - -@Serializable -data class SelectedAppNavKey(val selectedApp: App) : NavKey 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..313628b7 --- /dev/null +++ b/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt @@ -0,0 +1,37 @@ +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 + ) : SceneNavKey + + @Serializable + data object Settings : 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/tags/TagWatchListScreen.kt b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListScreen.kt index 33f7a191..458d6971 100644 --- a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListScreen.kt @@ -12,7 +12,6 @@ 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.core.os.bundleOf import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.NavKey import com.anod.appwatcher.R @@ -24,7 +23,7 @@ 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.TagWatchListNavKey +import com.anod.appwatcher.navigation.SceneNavKey import com.anod.appwatcher.utils.prefs import com.anod.appwatcher.watchlist.WatchListAction import com.anod.appwatcher.watchlist.WatchListEvent @@ -47,7 +46,7 @@ fun TagWatchListScreenScene(wideLayout: FoldableDeviceLayout, tag: Tag, navigate collectRecentlyInstalledApps = false, initialTag = tag ), - key = TagWatchListNavKey.toString() + key = SceneNavKey.TagWatchList.toString() ) val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState) val context = LocalContext.current diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt index 6c8c0182..9a42a368 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt @@ -27,7 +27,6 @@ import com.anod.appwatcher.compose.BaseComposeActivity import com.anod.appwatcher.compose.MainDetailScreen import com.anod.appwatcher.database.entities.Tag 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 @@ -62,7 +61,7 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { } if (extras.containsKey("open_recently_installed")) { intent!!.extras!!.remove("open_recently_installed") - startActivity(InstalledActivity.intent(importMode = false, context = this)) + //startActivity(InstalledActivity.intent(importMode = false, context = this)) finish() return } @@ -212,7 +211,7 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { 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.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)) 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 b56d3c53..474618db 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import com.anod.appwatcher.R import com.anod.appwatcher.compose.FilterMenuAction @@ -24,14 +23,7 @@ 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.HistoryNavKey -import com.anod.appwatcher.navigation.InstalledNavKey -import com.anod.appwatcher.navigation.MainScreenNavKey -import com.anod.appwatcher.navigation.MarketSearchNavKey -import com.anod.appwatcher.navigation.SelectedAppNavKey -import com.anod.appwatcher.navigation.SettingsNavKey -import com.anod.appwatcher.navigation.TagWatchListNavKey -import com.anod.appwatcher.navigation.WishListNavKey +import com.anod.appwatcher.navigation.SceneNavKey import com.anod.appwatcher.preferences.Preferences import com.anod.appwatcher.tags.EditTagDialog import info.anodsplace.framework.app.FoldableDeviceLayout @@ -48,7 +40,7 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, naviga wideLayout = wideLayout, collectRecentlyInstalledApps = prefs.showRecent ), - key = MainScreenNavKey.toString() + key = SceneNavKey.Main.toString() ) val mainState by mainViewModel.viewStates.collectAsState(initial = mainViewModel.viewState) @@ -74,7 +66,7 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, naviga when (action) { is WatchListAction.StartActivity -> context.startActivity(action) is WatchListAction.ShowToast -> context.showToast(action) - is WatchListAction.SelectApp -> navigateTo(SelectedAppNavKey(action.app)) + is WatchListAction.SelectApp -> navigateTo(SceneNavKey.AppDetails(action.app)) WatchListAction.NavigateBack -> navigateBack() is WatchListAction.NavigateTo -> navigateTo(action.navKey) } @@ -102,15 +94,15 @@ private fun onMainAction(action: MainViewAction, context: Context, navigateTo: ( when (action) { is MainViewAction.NavigateTo -> { when (action.id) { - DrawerItem.Id.Add -> navigateTo(MarketSearchNavKey()) - DrawerItem.Id.Installed -> navigateTo(InstalledNavKey(importMode = false)) + DrawerItem.Id.Add -> navigateTo(SceneNavKey.Search()) + DrawerItem.Id.Installed -> navigateTo(SceneNavKey.Installed(importMode = false)) DrawerItem.Id.Refresh -> {} - DrawerItem.Id.Settings -> navigateTo(SettingsNavKey) - DrawerItem.Id.Wishlist -> navigateTo(WishListNavKey) - DrawerItem.Id.Purchases -> navigateTo(HistoryNavKey) + DrawerItem.Id.Settings -> navigateTo(SceneNavKey.Settings) + DrawerItem.Id.Wishlist -> navigateTo(SceneNavKey.WishList) + DrawerItem.Id.Purchases -> navigateTo(SceneNavKey.PurchaseHistory) } } - is MainViewAction.NavigateToTag -> navigateTo(TagWatchListNavKey(tag = action.tag)) + is MainViewAction.NavigateToTag -> navigateTo(SceneNavKey.TagWatchList(tag = action.tag)) MainViewAction.RequestNotificationPermission -> {} //notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) MainViewAction.ChooseAccount -> {} //accountSelectionDialog.show() is MainViewAction.ShowToast -> context.showToast(action) diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt index ec91c09f..6a566d46 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt @@ -8,15 +8,18 @@ import android.graphics.Rect import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.Icon -import android.os.Bundle import android.widget.Toast import androidx.compose.runtime.Immutable import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toDrawable 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 androidx.navigation3.runtime.NavKey import com.anod.appwatcher.AppWatcherActivity -import com.anod.appwatcher.MarketSearchActivity import com.anod.appwatcher.R import com.anod.appwatcher.accounts.AuthTokenBlocking import com.anod.appwatcher.accounts.toAndroidAccount @@ -24,7 +27,7 @@ import com.anod.appwatcher.database.AppListTable import com.anod.appwatcher.database.AppsDatabase import com.anod.appwatcher.database.entities.App import com.anod.appwatcher.database.entities.Tag -import com.anod.appwatcher.installed.InstalledActivity +import com.anod.appwatcher.navigation.SceneNavKey import com.anod.appwatcher.sync.SyncScheduler import com.anod.appwatcher.utils.BaseFlowViewModel import com.anod.appwatcher.utils.PackageChangedReceiver @@ -41,6 +44,8 @@ import info.anodsplace.framework.app.FoldableDeviceLayout import info.anodsplace.framework.content.InstalledApps import info.anodsplace.framework.content.PinShortcut import info.anodsplace.framework.content.PinShortcutManager +import info.anodsplace.framework.content.ShowToastActionDefaults +import info.anodsplace.framework.content.StartActivityAction import info.anodsplace.graphics.toIcon import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -59,16 +64,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import androidx.core.graphics.drawable.toDrawable -import androidx.lifecycle.DEFAULT_ARGS_KEY -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle -import androidx.lifecycle.viewmodel.CreationExtras -import androidx.navigation3.runtime.NavKey -import com.anod.appwatcher.navigation.InstalledNavKey -import com.anod.appwatcher.navigation.MarketSearchNavKey -import info.anodsplace.framework.content.ShowToastActionDefaults -import info.anodsplace.framework.content.StartActivityAction import kotlin.reflect.KClass @Immutable @@ -278,7 +273,7 @@ class WatchListStateViewModel( val query = viewState.titleFilter viewState = viewState.copy(showSearch = false, titleFilter = "") emitAction(WatchListAction.NavigateTo( - MarketSearchNavKey(keyword = query, focus = true) + SceneNavKey.Search(keyword = query, focus = true) )) } @@ -303,9 +298,9 @@ class WatchListStateViewModel( is WatchListEvent.EmptyButton -> { when (event.idx) { 1 -> emitAction(WatchListAction.NavigateTo( - MarketSearchNavKey(focus = true) + SceneNavKey.Search(focus = true) )) - 2 -> emitAction(WatchListAction.NavigateTo(InstalledNavKey(importMode = true))) + 2 -> emitAction(WatchListAction.NavigateTo(SceneNavKey.Installed(importMode = true))) 3 -> emitAction(startActivityAction( intent = Intent.makeMainActivity(ComponentName("com.android.vending", "com.android.vending.AssetBrowserActivity")) )) @@ -313,8 +308,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 -> { } } From 85b0e5f9eaba255ec7d39ba1dc0edd98753dc057 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Sat, 28 Jun 2025 19:55:41 +0300 Subject: [PATCH 09/15] Migrate WishList --- .../com/anod/appwatcher/AppWatcherActivity.kt | 15 ++++ .../anod/appwatcher/watchlist/MainActivity.kt | 3 +- .../appwatcher/wishlist/WishListActivity.kt | 5 -- .../appwatcher/wishlist/WishListScreen.kt | 79 +++++++++++++------ .../appwatcher/wishlist/WishListViewModel.kt | 48 +++++++++++ 5 files changed, 119 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt index bce86ee5..3a8eb4ba 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -31,6 +31,7 @@ import com.anod.appwatcher.watchlist.DetailContent import com.anod.appwatcher.watchlist.EmptyBoxSmile import com.anod.appwatcher.watchlist.MainScreenScene import com.anod.appwatcher.watchlist.WatchListStateViewModel +import com.anod.appwatcher.wishlist.WishListScreenScene import org.koin.core.component.KoinComponent @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -154,6 +155,20 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { 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 { diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt index 9a42a368..32b5eaca 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt @@ -30,7 +30,6 @@ import com.anod.appwatcher.history.HistoryListActivity 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.showToast import info.anodsplace.framework.content.startActivity @@ -214,7 +213,7 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { 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.Wishlist -> {} //startActivity(WishListActivity.intent(this)) DrawerItem.Id.Purchases -> startActivity(HistoryListActivity.intent(this)) } } diff --git a/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt b/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt index c415dbf8..ade732ec 100644 --- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt @@ -1,7 +1,5 @@ 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 @@ -87,7 +85,4 @@ class WishListActivity : BaseComposeActivity(), KoinComponent { } } - 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 059afc19..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,15 +40,36 @@ 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.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, @@ -85,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 + ) + } } } } @@ -118,13 +148,14 @@ fun WishListScreen( 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.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 74d2dc49..e049e877 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,17 +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.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 @@ -37,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 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 { @@ -66,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.NavigateBack) @@ -123,6 +159,18 @@ class WishListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel { viewState = viewState.copy(wideLayout = event.wideLayout) } + + is WishListEvent.AuthTokenError -> { + viewState.copy(isError = true) + if (event.error is CheckTokenError.RequiresInteraction) { + emitAction(StartActivity(event.error.intent)) + } + } + WishListEvent.NoAccount -> { + viewState.copy(isError = true) + } + + WishListEvent.RetryClick -> viewState.copy(isError = false) } } From a042f4993c26f862d1b0f1784c9958b97c5a6564 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Thu, 2 Oct 2025 23:33:13 +0300 Subject: [PATCH 10/15] Upgrade libraries --- app/build.gradle.kts | 4 +- .../anod/appwatcher/AppWatcherApplication.kt | 2 +- .../appwatcher/compose/BaseComposeActivity.kt | 8 +- .../java/com/anod/appwatcher/compose/Theme.kt | 113 +------ .../anod/appwatcher/utils/AndroidVersions.kt | 3 +- .../com/anod/appwatcher/utils/StoreIntent.kt | 12 +- gradle/libs.versions.toml | 63 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 51348 -> 43705 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 315 +++++++++++------- gradlew.bat | 88 ++--- lib | 2 +- macrobenchmark/build.gradle.kts | 2 +- playstore/build.gradle.kts | 2 +- 14 files changed, 302 insertions(+), 316 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 92ffaebf..bfac4751 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,7 +23,7 @@ android { defaultConfig { applicationId = "com.anod.appwatcher" - minSdk = 27 + minSdk = 31 targetSdk = 36 versionCode = 16900 versionName = "1.6.9" @@ -107,8 +107,6 @@ dependencies { 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) 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/compose/BaseComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt index 1db28835..09bb20be 100644 --- a/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt @@ -18,9 +18,7 @@ abstract class BaseComposeActivity : ComponentActivity() { fun ComponentActivity.setEdgeToEdgeConfig() { enableEdgeToEdge() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // 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 - } + // 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/Theme.kt b/app/src/main/java/com/anod/appwatcher/compose/Theme.kt index 2c521e21..d7d3acef 100644 --- a/app/src/main/java/com/anod/appwatcher/compose/Theme.kt +++ b/app/src/main/java/com/anod/appwatcher/compose/Theme.kt @@ -32,116 +32,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 +60,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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml index f37f4cd7..4393decf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,50 +1,51 @@ [versions] -activity-compose = "1.10.1" -agp = "8.11.0" +activity-compose = "1.11.0" +agp = "8.13.0" annotation = "1.9.1" -ksp = "2.2.0-2.0.2" -androidx-junit = "1.2.1" +ksp = "2.2.20-2.0.3" +androidx-junit = "1.3.0" appcompat = "1.7.1" -benchmark-macro-junit4 = "1.3.4" -coil = "3.2.0" -compose-bom = "2025.06.01" -compose-material3 = "1.4.0-alpha16" +benchmark-macro-junit4 = "1.4.1" +coil = "3.3.0" +compose-bom = "2025.09.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.5.0" -firebase-crashlytics-gradle = "3.0.4" -google-api-client-android = "2.8.0" -google-api-client = "2.8.0" +core-ktx = "1.17.0" +espresso-core = "3.7.0" +firebase-crashlytics = "20.0.2" +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.3" junit = "4.13.2" -koin-core = "4.1.0" -kotlin = "2.2.0" +koin-core = "4.1.1" +kotlin = "2.2.20" kotlinx-collections-immutable = "0.4.0" -kotlinx-datetime = "0.7.0" +kotlinx-datetime = "0.7.1" kotlinx-serialization = "1.9.0" -ktor = "3.1.3" -ktlint-gradle = "12.3.0" -ktlint-compose = "0.4.22" +ktor = "3.3.0" +ktlint-gradle = "13.1.0" +ktlint-compose = "0.4.27" coroutines = "1.10.2" leakcanary-android = "2.14" -lifecycle = "2.9.1" +lifecycle = "2.9.4" navigation3-ui = "1.0.0-SNAPSHOT" navigation3-adaptive = "1.0.0-SNAPSHOT" -okhttp = "4.12.0" -oss-licenses-plugin = "0.10.6" +navigation-compose = "2.9.5" +okhttp = "5.1.0" +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.2" -runtime-tracing = "1.8.3" +room = "2.8.1" +runtime-tracing = "1.9.2" uiautomator = "2.3.0" -window = "1.4.0" -work-runtime = "2.10.2" +window = "1.5.0" +work-runtime = "2.10.5" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } @@ -103,6 +104,8 @@ lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } + okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } oss-licenses-plugin = { module = "com.google.android.gms:oss-licenses-plugin", version.ref = "oss-licenses-plugin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0087cd3b18659b5577cf6ad3ef61f8eb9416ebba..9bbc975c742b298b441bfb90dbc124400a3751b9 100644 GIT binary patch literal 43705 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 DHx0+F literal 51348 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

CflOl1i4(-2^1KXo)&I5QlgjRKFQgM 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 z zP_KwBCnXDLTSTI9$@zwgB(mp+)3lmOadZUKrV}r{V0`rAEHnwtTEst z{4z0MSwpdQle8@5Cr`lrN1_3bylt;)N9&*~)gHbkdj(`lYv4CIH6^j#3e+ZN*%r4p zZg$33*(p2*DA2_e+L+R85%=iUhDr-Ak=`KHpT6$$)x0z)t*Wza(?xB!Uz?RtEWN@j zf{`@lyD5Z42Y)%{=&Gwb2}W~lWv>b>)MjtCk*UE$ZcCZ&<7y#k9%H8r=Ii#}wD+9> z5&9`Cth7|LQFxV41b(DYezS@klgX;JxGI$xqv)ubwbFxi3}wTj^1*&ORQ>_^3YtUe zM!K5(sy9qL^?RqS@`KaD+8`s1CUVtJAqqdr@QW5PKGAg7v}bjvyUQrxv_p2MJ8e!2 zh_m#N@=Y2uW;mEd%>!>Bgr;dq@CLYneRnDu$Aed*H~6=rDE^7nyoTr=V&w&irh}Ql z4v{;o(x~nPx*ECV+QP&ciGt8*HMbDgk^}lT>Mmb%R3tlI3Q4b{-JMEp(6J)Y@9mrF z(Wf2Dh&=`H0>yiF9zJj}(=ye&amdHeww4(t`eEi0G`v-3712txxwF(459yYM74O^< zT1VQn3LZ-B%|%4~oMmV)pZLU?(Xr?D68Vg-ih6_0j<`1mHS@K@ks$NTCpJAMT=QcR z{XB@n+n^nOl`Wz-`e*dQx_xPmpNa$hH+PI5#e4mVYTq@~(PXOcF#(FG%4Ld26dNp- zL%G#_&KHwUE8o1T)`Zn1BfBs#5VKhvH=0`IFUf=raf;WE#rgsleAsulIiBw-v)cWJ z>pANb$6ne-^PTKbh>P63e!xC6faID_UfUh9N9xrR4=5itQxpOcfl4*-i_) z_bowR)7#XH=bMxVIQ=TNlQUBm>nJZen)M9TMlSsvRUf$MQO+BDNZY`A`?6smIS2&K zt0@h&9Y52chtkO!u6fLIaQN53Hy90}I!}Z2xSFdBxB+!=-)gIz@Xhba4uQV=Yloa* z3=*mcYpoKFyw=+EMxRr9pU-vT-+s^Nl=)n$MogGa-KKA~%}!IVW_Thy>q+Fy4LDES z^VEVd=IQiDX;K(Bm19Z|pUe=jL~k@;PTOY*zSR@EgO9x*0czd(#7XPWS;WD;Bhgj^ z#iW^FLvX8146_iq8?4h@j2bP>2Wv2}(I=93K^#W16`xO#z!Nmaj_t(#v$=6AtbCw{ zH)k-xlFF6WV9F$G{0^fgbEx88x4x}?ewA}_lXG)3lGDSy)uVc|lQFweIf+wSxaeX*WRPsMr2-`c z6$DvDb&RIc+{ZY^0r}Ld5*hdqZkbxTrE775-x4#H#T~w6I-@1c-^a((_K0T|X);1v z-FF4HVh`GV*jaU;#UpTR_xyep%AfVIh3{ko=@B}zGFmcKOqw~erE8;316`_>)_jBi zGPm-|o3UXle#Aqv0-yxvWRh<5@hdJBgHrEem^3VHpX)))^5q$XR0T-jU@i|j7x*$~ z5o9ouEmXE-BlOY-6^)J(<`9g0nN`l;5fpM1$-vTr5zS%D;DN#_Iee3|6<>}4+z+jl%JPEgyQ8G*%XGEL08BhdLkVKl5_0HP!}%zd+RHFA$~r&p`BFzrXz( zj{a9}{=fKaaG(EzqJ0`K6Q|Ax<8n5j2NaQ!>NtV~0yYpBnI z`Q8`;9z~*~@V2UnVos;_L7hAbg3v3N(O0@R^$~^BSG{NT(H&vGlMNirG4AQQ6E9$!mm#z6wU|49Xemsf z(%R#1V1H|1lFuKn>?%ov+2jtP(%d2s@%AxIX{Uo2NgBKFa*$wny#hZ1>zRwWa){iC zn*2z!U_Ljh1e8To%8H!Z@Kn)`$Y*r!>>P%=b1w7R)kMgfTI|yc(g#$v3HM9-HoI1v zdARCT15Kf6yvtSEpkoS=c}RWq08Bk?PLmA%Iz2H71#pB(wu@hEr;>A93iGp}Kw;K` z2knL#8IqTiGzHhy140FtH8~uTgx!XEo57F96gzU^QxO!vx5IW=VVaX$Ox*+LJeygy zKK{zJ0!brte1+b2>|md?b9rfGL)_3k1Mm=3{fho1=>>-ai`B{L z_ocFO$s}a8H8q>_y^NQPYrLbVC7q!?z3bv+HA|@Za!X1Bq*0A)q~s9XEjBg|e`@n{ zk!Rq@n(T#|vl^wTAd)EIQH6 zVAzzfiu0)jOCxPz_WPSE&C3|goIfia+FgrBSD7W!tUlnos&~AwyJPSmvp@Wef>uCl0}3`iJaLepUPKZ$153@d0?h zQt0r|Ii`#oc6pLwvOZ9h7j!ub_s`oEwXWeu%qFifR<74~R3;_r>ot>ZQ;#Ua)8JD9!Z|QWU6Wd{(tpDVU$5e6(WzAl39)vMf90jjz)Fu8Z}&4ktSqJlhbSr zN!%wfAsS1>BD*Z5=)1J6fIKw<6^QHW#bmirKpC7WG5=Fwp(9^%VzE5mY#G{k5T?;3 zyp);&A-Zk`cTP#X>?K#}Dy=9IhtoM5v5{GhOnn>)D7!p$7-UF(+)2ZJ3N=HFHB9B@ zx(35ZQ$Qn4kv5A$n3H`#39Bcnid-dHM3yO{uqR|>5-mh=t`e$XH5)NnYCNh!k;()4 zjV4;XFsy07Tm4!N{G^kYanfr9eQcA&YagxhVk26;BGRNWHjPXuTD>|9wpAVx%f!0a zC^L3=lIS~enGAE6sB>>;=*b;Ct7d98(lOrjlM7@-qCO|5Xdu?O$J*poxtb|S9#ibg zweZm1crG_)wuq*DlHHi8SsP=+n{kQT42GMbyVay?+=E=T2|ZLy zCUe~bC?Xy2VCo{ZwMIUzk_sFyDD`x+?pmN&#kvyshQkM${C$ScA8GGe?F={X7dP=< zy$ABLBhhHb#oPY1`)1xnPWM1S& zek0?JnD2}kPo(!R%J7P9oX7U88kb5{3|MlmVp<}`5x%?`d=8yH_K3??TbdqI(=?B6 zsSQzFC;tpuTIaG%6WicUBL~HB%3{FHVkv|wkHnhu$b8gTRM7!jt04tKV#%B5TIcC> z>@kc<@lfbv{&URGNrY1y>gmZ0tCebQK5IBKJntx%`T8-8Zx=5VRI`Gf2B zAk1ttM!0Q%mP_LzY@R|{G2{f>p;T??o*u>9HlX-0uYc^hR?M`2pco7~&b!h@o52-< z>xD4i$;%V+2fP5RhY{EwWeA`CYNDKDTa!NJi;Lhu({JBLq3<2ihl=Zn;L24kyRUAH zpn8y4Y|^-Ak-f*3rMg#fbZ~M{!@sO>v%}XoZVE&R+WrQHF5kfcS9!BLmk!AI*No~5 z{Cfh5-`TB%E^8n|SY;AW$%aUnvywm8?S63DQE<-2&_Tc6^JG=&X?lKK^W7RE0XrxQf7TikpEtBdKUCkp)sn z@+Uoi1pR>K1to2Dm)cSGz&jC z7u;;dp`{b>RBqN6Ct#M}B!<(Zp%lf&6kzKRH+D{odTWO{J;l?NM<5eBTfjZzN_y{$ z=arDP5yCnt*RlOBM7F*B&K`90wjZekw9^}|;Ixs*@G~H7+HetBecwguu<>wK!_ z<`4-i4uJ<}=y9Fl5$`FqhijY9Q|F;gb?@f6?A(P#=|c@tMmUjtjbJiQ+h({Zr@pw>5kdc;15jDHw9p3uF<~mfMd>$={LN8)sss+{auK0I_>-BPz2D+}>LYC?gE)!d8q2!_Yyp5A?@< zWH>yy9f++eDA~L662O65bG+=^U3I){ByzlkNR9q*iy;D@I&HSXp3D&jYdNTMmDJ-X zKw~SU`2?8^8>ortNvkfp!;|E;ZB|m$v^j|D>$6;uBAMUWmD)75#0IOkb{k6u!O(E4 z8iWLwb|Gm_%>8;Dq?-#_CVtU7(!np8;gb%U%YVSht5hPn)39cLuBKt0Bs}s~#dueQ z)>iPOSKV_{DW#SJ058DKC%RPRktDV`m9=JdH#t`_8h0<#fVr!mOcDGjd3CTEYC0fPFo{-U^#Wq)0v9U-APT=k|r zeEEjcxU846dJlSfc^3x7cCRwLrPV#d_P%W&cQShA{H8L_T|TVn1P|V1zs7L~{JrTOEoB-r)VM)- zJKL#<6&plyc9d+3GQ@g%u>e+5QBpIa0z~t`l}v@GhD+@-dGG_FiIHbDd0Zu!7H3I; z=kzX9id*wFJ~__e0C)1Vq{nQwRC;c(HNARh#9G%~WFs|F**x-G?C7x7ll^q$2cbz3 zIZ_gm)FXVL5WfPJ8Fi?_Bl-|USJ(1eW^ z&?I@U3~qwTW9W%9C~kD|&A?Ccnv$0MCr^qMCPNXo0GPcw;7-HwC!rczouU@Lu!zn=XMCHlh0it*90kIY54&_&mP=GFR0HgbTr`53?SBf#}4)O=Cvz}JPjGzNJaBYdpT$ZCb4 z^NADzv>$%>q{nYdiyY-CQ`H8E>b!?lJy`nnk;Kx(f~FMKH@j!bWOLDJv9-(WoJPVsbbVaqG(!QtNDiEmocCFeD+79Tq#cVi zeP1NSQ#~&29lP_KpH~qI|Hq`f1W^DgeVyp*+ka2t;Z}flx03i792g1K1s)AI^ zHL<>9r()viv)>^J`npIQq&<-f5*tG?nM}+`q(NXsWO3sbXRuSi`XUTtlY^p+jw17U zCy5NFB8lZz>-Lp08ZDuC-j5x)54sO1>uoM@2|XU#y*9^djwkB-?&IvXuh;2KIDp7q zJkD1FLiB-r>|`g{am+hT+MWDxe^?X|98@bDl1^eUu`7FLH}ZRi5L&E99OPJ|#u`HFG0;G%dO7eMHGMg>xSiVSc zd9Jh9)k4|m>iy}$szf+!6O|d0RFVHfVoQ~I13B_QF>Pwf#H_zLO;j-tnJo=YL9PCJ zr=8aKE=bOVru%iPzfjnl^;OElG!?ka3dfLH#+ar-yOtLG6x5MmZ;XZMWMAj$!C^Zk zw8yx6ey!`6OR{JRHj^rRK?+VWVdiYYqj7~^1_x;inWbjLOHn;hbN_zHYJ6;5lhz`C zZ?{Ez@{Q=RiQ=Nt{o_fQm%y`mxe4ttcuHM?W(#6}rd?O3@*kW{iwgdn&Uh4(GAHGC zVSzW3mBd4cVMeHlk_+T!j_iEn#tX>ff%sAdQ8%=)hzNgRu&F2}k_xR%6vmI{ctg6; z3(|{vC&|8?0@aQSij(R?$Ks2mG2A>flen#bfzX$$HN+$qgRn~JWG+DWGuNdHMU?{g z$OEHska;A>40XyA$p^Lylq}#y3*i*3qoAaOq_y_C(sItTau12sD^V0ts}^~;zERqF z^)*^9b%H#TAX}B5&<8{OFnb^|yM-Pk2lgNSsM?R6bK(*zK@*yTvM}$^e5!WuKTw*! zzVJ9PtVIUtpgV(Fl;7uiYHlone)rnKWDZH7{ARj=t!`ju+r@rrLv9n*5EnE2!(49U zyFI=ONBL>Cqy0YGqn=3we8&^)4XE_K+M{bX(W7fGH24$fde;_Ir-w#mAT)d(lu}LE zez<4bez^xz1*TF;%?nqQR#}~)yn=Gg8f)A@JAdse^sph{v023GwetbnP7JQKD-7t0 z;p_Kr{V^iBnm8sXG&NhwEw-BsNQu?5H7X z#vYYHz%rN{ik-Jo+~joE_>NrTuh!hxmztba-N**>)oE{t|1dih(!6=$i5e!=-WazR z_w!(#KTaB|T?_8+4Qg%Ke{8wB%nLMyP=LF$!u<-+?}Bh9zOoIz6}~T4kgc+qz88hB z@=%qp_0$Zd!71rz3*HP~nFvoAyJ&RQ$@jVpE-u{33x3*KtK!TET?NGX?H!DGJoKg* zRb>+#$jV>?KVMF)+GwGI1Ds!hAqdTC4-9>0C?2&#&NBD-GPVVib8tt3? zvPnNY|J?e^`s|^f;!_$F`exWi8^$%fqo|q+wLRd5M|e5cBvIMS6~1gZ;*}RKDEQ;S zVJ61VYDIaUJheySDw+4VRrAUgtDL_k_s^hTZ=N#x`sSbcO@QM781t6JIh%gs1jYAN zCb#5dim8A^?%|iyNxd;Xh(TD3r6h9_49rSBF~-hdGZPqV3{h)ckzprpEdgo_;@~U^ z7TieZ!9_@yp#T&oG9jFhwdJNlRF3>%A^R%-5XKlWK->K~8*kGCUONw~ss_PR)tq_bu z5oxC2GbYDi1ZE4^eWc1$@Gia}^};+UP>YSK>QI-8?9=M8IzzYWQ-Tl9kxOC_ z*YptDH@h&g%xPlLPUA=Lxi;`-%cWQYV!2=cmR*WiHq(~>UT``y6V+{%c?!PwB)+|KE5KZ7Nv&ZeIpTG;hd5F;j-27uRIc1Br93jMpU5i{E0ya6`_Mp5A`GHBme)^Z5F=fo! znH^U(;?)-hnbDd@p@(0Iq1fL}qW<;x-%tF1QM_>9pZ^AlHMBDS7jEufUk|;y(>wl# zKE-}(Cx-v}bpeCFLb!%bLble{-vAwHa~tDt_>;>wQ}#dOxJk;^vPjAE_VEa{ zynMkQagS>X{33--5CoVKl!)fy?`~b$$8nF6)vAenySBY_B(no}J28w?S6NLDGURye zOk8YC(@YHw>$<;xe*xD<*F$4e$Ris?>M0MAFSRyLHNkXq?~c!tXN%Nf3_1pjk2Xq| zOu$Q;Mxz&Qs%V?0mZm0mZ<{YUb(Ak*8l{ytGB?>5u90qgijKY*HDlZ*C0ipyYgVy6 z_%G2zaWyp?R-`wqTd*ouOeI`4S1NA0ICYHBdvh$Wj&6Hlu}LVEt3()&p)P7c32|z3 zsK_n~3N=Oc;kMmW4oc_TYG0}?V?)L(t>Yhs z=NV=s6SR)ibep|~88%nCAZtPwgcR$S$qX0o-3uL$${j*yoC-Mj%Xh^X*j;w#zuQAo z^&6paHv@HCfx#Xi+MnP%g-omVEXM+|7LyBqSIm-uD~XXW*VZS{uM{A!yL zlD^I$D0VG{NJ2g7N)$j6xwcFt#zCsuZ(JuBZB=dqcoUTbM`{!ew1-S+9MT5cDCV&{ zjwca_pB??Fh%M_X$|&q`1SZO>h5w*3>P$eo>^&>M4PWYFa;K# zg@V0t;Sduby^417_PgE~&K=%Xeuu{0O;bwZR_kl{fN#V_B>uUID5694AUE`SI?`k>ue*Ifw^RFWNTeZmPJA9*J|I^kCiWK+@IW6*K)}#UDa@Zbf zDKssI3@p-%G~iN7V-6_s$BvfUHv~~ptKE+Go)6Dt>-@tFa0EUCTu3MyBX0EyYLM|eSJy&=@?{~d-eQP;VRQuHWlYkx9K`>hp;~Ib;R?DZu{VNLKw44 zXdJPmhLTAyIb^?qTg#2VK0jY!asyFN7!H&N*MJOhP8L$RfKnK^H zVWfl^hUp(x5_0U;XD?w=IyeI!`N21JnA-MFVEeUJ>njG!C#i~cHW;Gz(v>Uh?CQ2Pa&@%U{L2zn!~f7)Ovz`+t- zK?Tg=xErxY6O{AbHEY9^Yg}ZDh{;ltDDT_0IL}!v{}Pk0KTLT?p-b0NiomM=X*1qN z6HMPy!T6hq4kJFQKromZXOfgIE*x*BVVw|)GfD?o8lGmKTgY@nKAkS-;tnaNbcm&%B zmvq_{UGF-t9*$kYw4j?qCJtCOUQKk_JQ8H42%!7`%2~LZ#SQX6;g{7OIZU)a6Z^Tn znH1oZP`E4xe%hCx9S%@X8E4|Pb*n5c?Ijkg-6#MVNm3#FC>lMkuPrFV5J{>-WU~+- z+abCw|9%wqd@FJ;DmM?meDw5Zi)_->1(d->MaaCD5MB!4Pkln)4TAC7?OLGPk7gqs zHszI#+HsxzA}5dp9TD|uCNUNu3}G{N5;KGsBr1L2J2aI(kvXOZVamt9X`H_*ptJHP zW88NI1b_el@ceHo;2%R@@!MmvG5xL&JN<7`;(r3yvy`U4*GuG2lXhc$>%6-Hy(WK+ zJUJr@d~wOp!Z3(B1SIINt>VjKXmyv-tK{dJp3w|2&s)GS(xHZLm-mHcpcv~sW?&FP3<20?NT zpWe)v&87i*nfS2BB6qdM7M6Sy1*3+&Wgjnmw$dAUDM-kisrYpk@SO7_kSu3Zy{8u; zH$p3}kioJ&b&VC&b_;lmx_wvh>W%Pb^F%t$&puqJlIrv>)NEV#wyh*dXb+kV`S~`l zL-9<=c~qHxD^`C>yFil>wdKq~H14Q>wdDLOFAf!6<*V2s4 zHQ;qyfxo0-hrz3WC`S~<<8sV^?6CIb97XPgL-+_p?e$9R{8Ar(v_B$fSb5%FZ?-4% z1Tf@f5lv~XIv!>dR5x`CdXCc~(7}7;E}DDgd@IeYoT zWUW`C9#1Y4G8vzkp+e8XBES2yo;yC_PcqXcs1xK+nO^iA12^n#Ln@RtuAvbVGM?a% zf&(7>hz0yjy&tl%FMo@G{WaE4h+yu-zLm4o_jvzr^x)rS`|p|E+4}o7fp5~Z@qbM9 z|Cr*F;wB}57?6WxUzrM;nl-Gc&ibwzmBE&i{6qceTWgEnoG^>y(u5hA&Mey~TW@}N zkuyk0q0soNZyaQAylo=gecrx;?m$l>Las3CuZwJo1oUtm`+A#~KNOY)B1zIOEWRqe#h@+8LsjFf%Lrtp(qh;`UYyO)ANo_OfKhkgJ|A@uvs{ zxTt$Vsi(T_cKvmHrR+zde4wFVQ0{$24Yiq|D;P~TPcYoOIxeSfk=t@=c{Uqu z^}!nIK_;^LC(6QMEbZrAmU;h8Z}6d+eGPvr^pNk{F#cCFkd)2$Wf%XLhW?>I{Zz02fpUvCy6N7xu8><|7R&*_UqC8mD~GuJEw}r)WoGBW3x7l@9j9_KI?j; z+wpDcYVa%j*AITKt)w~-*Xmpnf&wH%L}?5HwMdD(J9ix`9c&$~Vp$1vI77ic1dQdK zQfLrYhKC^fZZ$u;-EnEB7U{j;ee0gYUdlrrUObVW##a5_jNN{=ccU#vURc}ueb>Ra zJVP70e%Je8o$qpeG0)HJczpQ#=(veDh8WJZea{fT$lTq@BXjPa^f6*~Or_uMA>RR? zq@GDC+?D!jh%@2kDhn;uj(jb#jzR+y0#{Rl@~msj&s<~$9kDkN%q|-);+7CJBgh_> z)cVXW>xPDynYK(*UwtOO+Xm8%Um^T$H3BOpnNj&|g;OEwZCBxnu_sOH z^eCB@QV&QX8r8E_*?HmYtm#NIRS7wcvv}z(fI%ri*LZ5JQ-3JJI|2_81I53y{RMZb zp4q-BwHr@l-Pw3Q*E^1?!|A>{=B)=|K&}V$y`_7~hMswJerKk^ZU*_7tJ(|G`i+gXpTXq#{KpWdkF4MuWTCm#ZpRCkvcMbTcfFCC)wOq%IlS zlnw307^(kvNlz~cJJHvzPB{=&qnfm9X8Pk4tHmmh)KU@#0HmA4Zqc0%4kpy7`Dw{R zGhj5`XX9ZMNCZ!hQg^gH+UZ6oGbm%U0V{fBW87=-d!CCSY3V6%63Rv`LL~fy*&)4Y z6l$Coweeu-(anYsXvUVQwYQLug8j(e?aOX)xK$gknSjwptVxEB_7S70K|JE!=2bx2;L#ybB&L8&`F|bHty7@Sx!b57!VaM!@j8EJv zF=?Z+gP84LRVQ-q28YZmW$?uAVjyU3GY8WVq2qF!N|;(!MsVR}1rTKu{*=_IX9}da zp?2+6x&}CRKTg2B-kL+lS_6XFIqL1htIO`QT1ZH_VJat-ns_&;k&nKYavSG)BVrT>ivbcFJifDxISlO&`>BfBAw#OF7diwC@m4o^aMJ?_P3y< zgBfmWok0nE)>?=uH`#7rUkKL<)Sp)zoe>+qG96q}>+_MH^pI=@1>!$&L3WvRg1-VN z2Z!VC1A3fh(Vx{fK;O)8AEu4b|m+aE>o{^|?H1DEU2SvurKOqr(VqKscdqdci z&{6iQ$!^#9eVKCw4-4LX{acrgZHZbp`K{U3zq@p{|9y}0@7>8?Zr;2cvX9O3tUM>W zt>O)cFf^8}u`fO}LZ$&K8hskUts%xF^{K|3%RtU9+-`(!kGR3}MGRr~I;&%?~fNP5;cqtlH+Sex))kedMD9{~?ndy+0e1o24# zzWUt2IsBCJC+}G!@r~6JnFRJfZlSou?#S9{2`;BxN|y$q3ZJ_@ZG^c4yw<{(B7o5t z$Y-*Edt=(M=|kk(9>8Nh5-N8fBsT6jvJE1=N=^*+iNn&YIX4?_obW~kJH=(Ewen4q zvzf?C;#9HWe5>@#rQtd5izMO$p`X!%1}qyP^{3RFrs{v>ilh?vVXq>Mygi#wJfBnJ z&TtC2ODj^;C$6G35+)EvN%GapzY3J84W8)!t7ms$ut>K1T_HB#I-2i)Qz6PWmj8o_ z?ou9C`0nF*ct(l!8TrBCZ-YX~N8!PD^9Vx;i;9$yHG=B(mWdVjPmF@or4w~;bhX4$ zVkpske7|;vmiwZx*xGA5dD0*e1WD|7kG8JXpEA3>uO<&Zu3N4F4(v4rp!Xp;>1PEh zGU*fg4hDM@{mmzY?ODPtp&eHDvvCKph29Zd$J;wd0in-;)|WPoBT~ja()0}m?V~bx z@A8X|A(PWIT_j0t&{U;0YxYFXcJ84Gt}vlTlT6=1rqwrC9W1jg*FbRwp+eMxcMB$X zW$U7I@Z&({S-V6)dAu|0I0QTgO_wnG#%1Ed&rvBVlIDu9c#krYX>|^eTbrh|6)ytx zRy-}@#erlmj+^i2d|D6FqCZkHX%g)aQ?s{?Pqw^ubR422C0ckC*s@l0YYi2H&#TVX zx8h?x8MDk=WWx>d=C;gpZPp_hboPlHz5@tO38F)AB#c3^|bYq9{FP$tF6(ZHSc~@XG`RQo{A2MeB0+NKp$~2kD=t z=X>cFk=Fqh=JAuQ#f)BeS<%AvnKvz%g41Ds2$9jDUfX!m>K>~EJ$^(DHT_tuqhb)o z>w|q&3ywvG$x~Kn9C=zGxkC`o_hzp9Xr!8@mG0Ix1dDB~;|XlM!0lUm#y!B{jEyDC z@Rw%#L|}Xa4)PXdd-LagL@7Cuu0YfSFa`KULTmIXsYUTZB`+PCZ)#85$|(UhbBVit{*wf5Ybs~t+1G~8R zzJ^E}sDO!ua^Nle;=Y9vLb)P!%3?}!TIxr0Z(Scyoex!qMR1LZeT5TFuLDA+uVk-6 zYd&HsMyvHw#R*|k*^AkmwywWv3(J^gx>gJrui5 zkk|p;Lu?Gt+`35(twU@CQyL10@!L^6mqEP@DO;iksHV>CgglVixrC?%sZduntd^;C6QOq4d$K4vpo zxSKbfe)#;*lB-r6uE${6qdvRn%SJP-tjUX!5|s6}YwiJ>p^ibtnW$b>Ss>6^$Q)G$ zv=)a8ByX&dUnaCNkf+IcY$ehs$03~R(KvJ9c9My;{3-S}Z^@_#$e!jvcF%`Jd{w;Y zbzX+m)Z{RzXQC-+JFVnYkP89oH0PStP;gpX!;&YBxMbd6dj(S0Tmr_9tNEd-3NB8E zq0vL!&8e>;&}YKdax*}&pj$e*BG=k)nO<+y?nmt}D>nbtpCUCtQDJc0bl;xqDLZl& zdsDuHZ#CD5x|^?|V}uOCRVO8??ibJn`4}oDYDNipwU-_F28pXD-TU^;FX(D0YvfhB zL*z99yQCF!ZrseZn7qv^F^h^UhPSW4aV!Ui&Ph2r?{Wd0E~UebGPHkkg6^97kD-WU{bVZ{FOT$3|X= zDZ;A(5}N?lF}A88Ssy+jw-9Q4DY>!()8+oYBVhZLJl@|} zub|bkp!+BMF zJ^|u;rX?PM#^SgJs!)km2RjfPL|g-`pw@x=u&@cbQ0QuY^Ztv1U!SjGTWfLqj&KHE zSA}25?K2U$NA($M!C{BoMGP99!V%Ck!Erm+X&>BaM;WSisn4O1V)VeRb28W@cZP{5 z)yk9hd^M^RS-B||DjZjVlbk;;>nvj(BghlqHgc88&N~5=$%q!Zf)lb6EVV$uITBEk z+%Aq$To-}3GwrqiC{21*)-R`Fs^pzM)nz;McTSanJ4Rya&&REX4p`(i^XCe2XG7^- z-2h6kZ!V0!n#jO*Jg0MT1jtX1=IHdTF*((rYVTL-JUNo9*U=jGQ!gJl7B-BpJmc)G zUUeH=rB9NwMY#5npF)n}PP6`j?}}>fsvc!*UI56(C+SrgS{b0d@>mVgrk?R}F^I*$ z)z7X$I8y)A9^%jn38t0U8VQj|)$ zdqMc3;q1~!<-+C|=^)b`g6$qC{uToxoB_Gev0n33bmX(rf~WDEW_@<-aDNb=cW{)p zF^M{ga}zK1CXIQ=KbkgzR46!QGoOapL-gi0VYnm78o@0B#i zqT2pR_ph2L(@JZ)~S8~&-afH z=pA@nFQeMi{=wpq_z>&hi!!CTOa`NJPixQ?gePF3Zi=MugBDzZ+xIfUX@e#khw>Sg z=GXg$mffR)`n!*#BWj!WS>T(D8#6TZ~FbjtQY26+uCrx;XW62*X5=Y+D_5%cOo*7;Cw{HeARWc}jhWw1uxaD^pENYaZ z=-$U(fpAO}SP}}_HG5U2N7m79zvK?5g?VwtOhF$@5Ys3BN!Ui>(MNlc5@cvfsLIn0 z5@^I=^7yOwMZzy&HPOiX%MT9uSQPmA8N9WTmAbGsRF;BPpJOn85{=r?nA%71Byw=| z_h1B3pE!4vN?metRmnSy1>BhNiIx7;pExpVcpp+>{l|Z^`iYo>9Xg}o>kh15|bXzfI{^F-wRoG0s_?j!$#9ts&d1ghuGrMPD8O&(wn9%AfTk!5y~XPfh!}$qcu;dHq~MaT|5ovZ5&g2uvy5)igF7(A$VH;|UafbAkfybNBhgj7 zGR%ziy{z_PbxH+WC;`Z*3g(jPxe_+q3|@z)M?Q5>uEoWOiW2qJ+Mmy>NoX(>fnVJw z9Y?}N&w>Z*~+q|kXM#h7L&@c7EJ8&4PzpTi7HLyB{U_HG>7@6R`8uY zusG{=HhSGSQld>;vYt$rnEex?B~!x2UDe5B%+ALW9a^ktByECC9absD6D$oItplTa z#vrRbXzRJ$nAl9{$AdJL3wams?GK64PYcNe@ue-2_vjoOF0C-W+M;#jJlSkxERI;! zs~NK_*WO@%&I9?day_4PzW8>|qT38=(*C#wSO<{wa5*lTT&6deWj7C4%QUy)AxNCN zq1(pI{ER1!Iz!|`<&4H(e)Jd87Q=-jUuk$T=(CS>?yZUjyTwJ(oxgSV5*lQ4_JUG% z?u@df65pmVMzu5zJb8xguGsT@x3MbH9(;0s2jEk(o5AxeIPJBd-F)puFr^tfMonI= z;hZv%9FDm$^pR;!1J3+vYmCm>DZvI7;+)!nz`^SYaejx!qV%cW4`8p^M|&n2cAW1z z4kE`m^Z+fXrcUQQ`oJxIn9*}4*RI=in(dS>97K>$1wr{eXAgtL=@SLT=@S5TDcoFF zh@XjYDBC!VGo>>ArBz3yaV0u$NEneABfymRf- z5ka?+s#+i7!4rrc9MCfWl+-T;80Y&QM1MV(CKQllt9K};6jq9MYEIJIqHNACaHFuh{IWI0$V^SgC4 z#1-tP&8Xizg%#?Q4p2S%Q`cMXr=z%jd#Vz0OdW%BzDN`JcfG4;3*$ZN$4)=(<4W)8 zsImK^&BUPD!_yH&iIwt50Hgl;9h2{iZo&}Az&-X0fHcf2Ga2C%#jTDEohYQ_U_G`c z5{Vr`{FEV+P^^UFT&pW#7_0K9!k*JkLZ*F`M3$3*?SriNR7k@>;nqO+>Psj*3&H1) zx9zxQz@!pB{Dwd8B_AsU3?-c!JKI`@S~=ZO$fFk-(UG2kF`~fQ@na!@2Z|UxH>{0X zd)Zj6uCyua_$f+_=4iOvt@lqGFb}^Qg0`W*h%kenRY{0C$cAAt2!6RcJOIq%5)FYd zOe)6RvNw$Fz(0Z1r|&4zqa&oTqI+R7#rLw)Oz%n%&Ym1oWQSy^p=dO~sO01gK%6&t z1e4`c@~jfE+1bg+Nj{vyikeJSm6NZb>%H;xaY~4wCMOBSEqtDu0 zUg+@tv$e^TU_6c69&UE9Hk9=%sD`Cg60z!}n)k>hv=vmXjG!K0(Dbx11|rON53~qN zn`J}X6#c$+WlnkTKmq70g#6ZVf4^oRs?X>ej-l=9bYr{rixu<;DF9*BQcT!% zb71%P0qZ&y0m9TRq*gBXG%?*M@qBiFaUi!(yIb18Ah^5_>hz2BA&DcuQsd3imUnfT zYeBaV-1nJ1=GvVCw~3m3+D!OCIdI2o8;Tu5&)O9w{;s&(DOV7T0`U1KwOgo_?Y{BI zlbFm*7K~u__B7iRVC}tj;$x96jfa`gc{4Y7He4tY^5 zSb#>sdr73+E74q=Q=OZ3V(ZGkpH%v5V?9EE#mehjYC(NVEzbYiK+8GUS{NHTeZSd# zhbzsE9sjoQ{#)WQD_%;rj~_W`8U$F_i%+gU|Dp#N6Ulj>NIsG(pBVi~h%1@FIs_UB z;!9GMl=l6{C;2{dIm3$ZKK0dUCdc-JOR?=WT@AovohCmjmb=waU6L3@$R)N5_$m?t zq_?QJs-Q zL7OUfeq3wfIaD;yxfB7uK{kz+ioryN4$jhQf1XXvyylk$g9D>1s{ZtdPCTlgtm0G& zpQN2k#hj2VOFwUrBqA+=MkC%v2SsC3hUkWs9(M8lSqkMOCk)~CTMIP!CAk>&2!V!E zU9}SKbZ2s|Ln-ytx`+e0-Bb*tro457snUfLS+HSFkIV3D#1f{j_ZMuG9eY5QE0{*z zHoFqN=@lO)hTMaG@l-~dbz;JK`u*p*Tjks-W4fC}CYz1~rroffKi}}!eeoJ=sO^-* zoAz@LL(7Y>Jen%MD(XI&K&Ay{KJe)j9dj7tgkJPOuJ$3FHc!f_AY&*~tI4>@L-8UZ zjw|(Ct&+SqbwKK9xUz;k%qVoVW5~C+&oXS_$-_{S;~ZF8Br((1Lj4{Ce({#(7g5FO z{0BPzU?gTCiI>)&hbwPCGiu4`(~%%1z6 z`yy%|>Y=n}v~}=w7^J28Y#TPRedau&UT}JIQ=LW!c|sYwpSy^!Ui#t$Gt$-ElP+d8 z6tiq{mr>gd0ZqiRr9Ml;WfRj9@}wtAIa;d3E%1UB+$mbcuxcd!3^kQbm#JM{5b-)& zbsM!7c!@IF9J7uIA-aMQvu52Mfhn>aQ9@VQk+iGANS6^etaiGGlXJK}F{Fp(1(Rd} z6Vl9}QD+co=fH^+ReV4}yH;w01=i$saMogWg{G{lO(=%6%4u&-Vm0$h7!Do#fQGMe z^^g^WysSHWWc$penR&CMBwzf(Ob$w&FcPM4V(*7Y+s@P1l@+E`pZDmqY2KDEnS}O~ z0MsvsgTM3ZU~`NdjQ7MpwiG_W;asA`J~H0vyS{9q+A6&F9I z8Yn6=ViyFdo6j5-vKS!B38FEC2F-WU9!s5~$MR`fI(U=Lp<4te4V1DoYeaH4%{^c+ zWSc9p`Un>3oYofB*3TnW6eba^Q3}^7u6@vlZZe{93S%XToGZOOu_)?cKtp;13_Il% z*G4Ztr(@q+VjzD5+{EiNH@3osT_h)fwXO~0^MzuPBxc=YcYe*cfkmfd{h?>gh`k|Z zKwhpfZ9pB(wBogD!1UO3#dJ^^62Dmu<&2roO!8^@odbBwz$JZm!tL|M`LxJG@d+Ca z!T}Gk1|Nx5Db-HqHoc9vRB>Atxz}}iW{@v#hCyCcR6t{8d=6S3R-(k$t^p&#P@p0R zG-7W)gdr*4pvz-=U)_7bHxEMVLABr=;?<-~SgliVjWW~}KxbSw|Jt^kb?e}e!B0TT ziIb6d6sz|9Vri8SY?3gZX9W%K^5|)p&d|pgBJX{*kIGTF2Vtb3NP%rwGC-h$x0)v1nAY29^qlo z68EPd-&k6`JM|_t^&YYf2=i)<;eLk_IUc?AV-Og$_&}YZC6=fGZOShNOq{7fjq^)p zB#4vS!)e3J*?LCs>uhOsli(` zMRr0fN}ZTY*gH-ud{jOnf`c!MI%3#)9?|bW+ZFM>$>B;M&2cI_5_51M(Uu=ND6bo1 z*B-m#Fdic~>U@tIF}nP$8whNa3F%MO3NWeBsU9Vp@x&iv3c*$uuYIqZTwSN}F4QbWvgys&+$8vMgQ=eoAG51AJl&U`X z>c|`9EG`(Hc1Pf{>1K%`Y8>Qun_RlF$%e56L`)IPibkaYeY(~@$B3DIuu^kYIf6Ec znX`O6dMC?wBtFLo0!u@67;bp0mM0)?`5kZ*%iyoN-^^TV``{s1G`zr$F#^ZiD$CI! zz-lD1YmMFfWN$s>?UT3#Q{{kFFB)i%7dxs9`+)f>Zep_Ie8-`P1SkId{lLqs2ZNK1 zyVr4)HK+CSH2HqL(uDMsL9n-A_YRJ{zlsyh0v)qK8QbC@v-I2Yh~#gNm+fq}oG!(gAm31IQy+X>I+86Y2hR&8zo zYHy(oF|un18&)}_)Z(-i(*1GWDr+tT|34yC6(h7a zs>eWF+?raqB(P?DN~B6MS|sUI@3hpavc<_@^P?*GvP7NH9js5=0G;VwkY2Y(UTD{6 z73^T4#^7Y#@f?gW{;?4UCMf&$wXO9n2d82Tf;e8cL9N1hM%x)O@Zv+a&^IjCEC_l! z19|$ctoB;6SU{^SSd%S-G|59^upX(ap0e*lNS2^SFr$q6<9+-D0E%WromT71_kmu< zNBM31un7kT2#KlcH$S^WtRG-o zWWVT2h!&`OX^v?-SjJ+xyi9ClK#i@BDUI*P>JFo2is~m2X@CZ$f>1q7uM70=s&CLt z!IH2umt@aWSE!t*S;8e4PtEKkp{2ZIVl$hqONbmX(9!!s%H)c!{E(6lOM`7*;V`tk z3LUEy6t3J@lt)D^r#eu*G|ZCjaO}2iC8mMTrrTCPTkDCSyh27Xl=DHlcjD?CQF&ar zR#h~H4P<@a!5Fy$wDt~xY9Y={SsM!Eb6*y0h0&lFSP)}wFI42{Bq_<Kw+~ zOcOS^7Z#xM>Mv)e8wjYsq8jk~yfhVA8ph^4PlX)ji<`>)uyr?A%!+sedd=6kBSU`A zPR~izcPJbeIS*-sbzw#|4mcL7b-}rrsN)qZ>2FN(=uo7dX!yBZuZ3dfRFt=q4(N+c zmJ#rrN6UTKy724^ysspBpHT3bK>aiC}UGHP-yl{-I#72K#LO zb?D$H(syXUdDSX`R!b(L055u=M*2(^B8_R-JEW+UO*%X~%)<;)!m~-xf~fJKXe>^K z<-FUvjaRh$h3|N4{A}XMDADQS`R{PS)HH@q?-4y{24p)LofX-7}G+r5g^`Qq7Sf~4~Nu)9(V$~$#sO8iE6z^8OvVMUxM3=!^x z29#yo#tqF|9Vb=Hkm^C#9QVb$-DOcYo%ik+@a`D4wPVgflqyOdAwrj9AMz*6?!}s? zF^av7mH1o|a69g_F9i3?K0OLtkURSpY(Kjp$1`ibR~Va;&Q2aoBay~KVf->d(ZZb9 znjVxiNLe4>%Nlbv&aPqIOkjx@YRK7dDN5IUVV@+kQ3P}2vNPp#=hUyvUh$q3C&$|( zX^B`opBa10m0n{>ARi~^c?Qf4@5`F^dDGVd54cG$yt(lcG9eB8+`zEunt%Xc)WDHVgIN4WD&~5``p5BUde-DE8Y;s zd4A}nGkJgK&P)Xd#H8eOlZq2-cahfBBqSe`B+yV+nO@j#$(GDoIef9 z?}f{Gj*sFGOkqy|wT$0&j_Eetk(H59e9NcytmH)eB1tvduxbh?&LwHH+5eu8$8CMH zs~V>AvwqP2N4z`?fdP`&jW+Xl{#|&Zr3aZ{D2URyDAK|ofLBAAao4y*S>q+?N`Ex_7 znsLH5N#>I6h)!^L#k_-}@{TYmN`ig6nlVY0JG*Nh2?3`_P!>q`&i8*ERAne zc=L{y+FC)5do+1a-~!j*t)BVBGD5vCB6spSeoA<>W9yzGKvrSYP`@bDiZ0__ik2O( zA+8YdMhzofEd|yyV63_$Z+HkMD{=9S86ZbgXCIX%5Y(&2^11hV?*CzkIaa_xK{+eX0C4%R-kd(`f{Bwh&0RT=M=PjDlQNJE{JCG4vfb-5 zw(>y`a=J`Q?_Tk2WAM9kz(N~3D1H|ugeFsT&=9wWz%MmHu3thbY3bBDmTMLD%GQctjN&kT#ftTW~PUF zM)+jO+M({=A;O3?4oukQOa{4mOHcP1Y1Y845s1@bHs>(4=(VV10_K}dlXH10D7wp5 zUP(!)4B0)_%P}GH>T<%|QPK}`pks>~P6Z_~bivI7`&QLxY4r%&^_#nPkXm8wh!M{T zy#z$oY$PZM0#hcyf8 z1BIG1=o9QUDj~6iI*$FYI|qi2UD-wc%eCV?mQY{Mws_o#E0Gx zy<1yQ)OW9DsiM!skkXdhNVW^`MqxisW>e_bo+adli`aaBQq1yeuIaz)!sY`D=JXNlrk3gRQFhR(3!`cJYj=xv~dbnAj(VH zdu(puPWnL{*KCDJcc^aPWY=Uq2zVYK+=hZw9+rm~xi>eru3yVZ*VOfM?eZ-s%6?8& z-;nR$vo(p7c~!%TQp@rDlj%#L!xm&AKO)gq8kRPIVH#4fn-PZ_nfvotw~g_oE708R z)npVY1-ENKRV%-jG^vMlsYHII^1x<^2toT-6p%h~meBUAaAyApP?5&~)UkB!U@ETP z?K;v1b2kV!eqCQ}I!a+{PJIl2_*9wjzJlrCOW#HA2en~%Np?Sn3mI&cBW?+;Q6>eY z1a_eTL-MogLIUt0Uz5-MZWj+Z4!4l1H0T^bjaHgS9U}rwSjx2))$!SyVV6+Vu46}F z;iDNXayQlxhv$2CEDNUeJQ#-_)#-w+G+V)A9xo2e(&qOw07nK5Fi)Q*ayQq8yfan9?JrQibZ&H=S{>N>(@39VRe+L|kJYW>s zn-@AJGb?~W)(vvtHIiLmGlQck&U7h@qu?pgwWb?EpjcKQUOSxr%etcM%1CbpNtaQM ztEE+r?G@X_^tRUfXEMD(;3$)rl?l6KqRI?K1fkBbq^Jrpiqwps_dKcwxQo`ESi78h z&|s?w>Ngh*mhC^1X;hn;+OHb=5!eo$rhH=U`fOMERU($4WltTHPNeJBp~@gQzj-T4 zzkYqTL4C6`(nU`KLR~7D;N715bR(KQUcQTeTsdZ z=(e(XEFd(##eRB5P3N9fo5@YBt|ds{4HhK>Rtz}}W<49tXc&-IG=UHGo%B<2i?YUy z8JMiD5w6{0v{}J4SF7P?qc2Iy>E8Y9LmN^3L^2}e0|GwT(jMF?vk=Hr!CLe zYmdTqrqV0v-=O;izw5xdHeLJldYO-n-B}qUuTkov{G5{HhQV!TdjBy~d%fhkY}cVD z7waR<{(}_0Q*6`XB>|onrPxK!NB-K!@&k&f+l+o5qM>KTaH8@?A9u~*f-KzlOyU*5 zd@gWb2Pw^r_3e!%_yNxgEgq4tgTjj;4()IRMnX2e&c2Y7!{aK3`Ah=Psg8LeKrmDg z!Qfwouz^sLu|w`AeA|%uPDspP?rQg0IR>z}`Rt2wc%WRnFk-*Y=k@5B$3iToQ6_GJ zLaX^EHvZ4`RH@<$X9!HqZDdh-a8HjS!$Z=?L%GYBK`>ea^b>Zi80(QOl4D5eF%0ZD zG&lswz;^7UC}ChCXN@sOb2j0|+QBfznX?jd-(`4l7_~idrxYGHIEVuD`4oWV;9vFm z@7?{o!Qh7@hWw$_HwWZNxZ0Q+&B1u`ByYt98hwg&vVdMpBqAUr81P5fLzOr)$K>Un zo$PDShuGKnIdAj$rR=c#3ot-^m?;q%EiZZ4!)0Z$L#zLXM0QY>#Z~!`?00VU=^zM11& zTuYyI4!#XR6~Fh*<1gDVb?SfSKZ`cu%#&W2BzQ3C&8%pQiUEbz!2omWq6x~E*;vhc zqIMd!_Z3Rg(&ej%W^?uCSf4B9NAZ9#ZFEi>^vJEqFlrbbtpX#bVqFX>7^LOg^y5V- zfosmRw~BqR5)9=*VfzUaCo!2e6nike0LN1<*DPGdk14O1T!sWWEV7evc3Lov=P*c#pNe|cXIb3cPF8PhAOB_)+OlQS4PmW-8a zl$^z0qI!;QUF8GNv(loMGOs zkR-1Qi%ie@$WHU6U2UQD#zbSo1j(WahL4o$-8qd>=*vgk8iJT?#(t5v(0?~K+&2gk zRRBaD2>?NVxqctk|B5X0Z!DfAO3TVvg2<1OmD*jEn?$VmG`TUr;3A^xU?!PHPzpL- z@AJH?QJRRwRWKbkj{L#f_WGKR(>9vQZli*5x!o_1PmX1d&El8`dRaFUQkWdKMpC)j zzBVyAUXHfCy9a4Uaidy;K_py>9SdG;78O(J4f0hiK3#KdzG@AK@l_%wUh05AoT(W1 zhpU+PZ>sN0{>tY@-0{8ypT|M~4)?^XGuixzn1-+`mr_UgbzG*t(j<#(SO*@4rXl=R zXvpALjDsGFF zk|gG3i9%W|=8`pAq4(~BqgHk2{vNzy(<$0JgN1!U?~9z(ne6;0Bga3d*<^Iv1f_-M zn#oUA=`HLtXv&xi4i#Ydw}RU$Elg>ImlzAIj#q+3btv(v%S!}XSre+ANu_I_ z^jzwh*Q;}nHim>0FWP;P<*zdnlt#)b-Ee}gjSHrsa;`LzG*;ED!0Dd+a$cq7(wxL` zMwmCGz_fJn`jB^2Av3uEWDRU{6f4FoE~D#2hFe3~2F$)9flYD9h98b)Fi9FKD@3V5 zOlBQr@l#Hq{zNf&vGX{C$jzYfIz%{8T8a;;+R@!9zM|5FN7IK{%Yu~bMZbLgGA6RCHAI^yyDP)>2Ie?Q=Md2V!P(+I z5K`VBO#L-qFA#1Z`5=3DJ|mAnibX#xM*0Rcc>gtGxW1cTne%yQ2stf7N+AJ%uReT7 zG#O=Pcb|ApyQ!u=3R{(*yJ8(xewy|t!Ps!LeAks~z*j72`o`TgNrWTHK0501O{R!^ z*rKtbm8DDFydb0v`RjzJb#$V__5%~avH z+L$jTfSkGZpa*q#UI@wx{=465|>ewTeSQz^bwj@~^ z|6T!Y`mLe@-|V)pZr4DDi9nO}t9P==xK~#fHPF$=0hr#5GL#`SO?7tn9d{)`TZ{$pIwZT|lC`8{_#q z6l>GHxP!Z~l;tEJo61S3-&TO~?0WMYlZ?ilN!aJx@($?#Y zK(UC|?f{2?(F59CWKp-oRF1Cz1M4aWQ`@84BhXs}DhfRr8Cie_6hGW8eR|fWe^9b0 zbxwq5S}zSXskOSt@rQbrP+y{iVO1MJiQPnoP=;p!y}D zZ+2y-epE2PlUcd0A-T$ouCD9SDNOY%$0H+kKfgRBu89+9)Jx1xQRmWeM(%NDXHUE5 zYMr``FPEiQVoqOo$x|3zKK45M>+8D4&wh9xKN9AD6hO5C)}o#t>rW+IvBGhSA8RLU z{8rNk>T#g8s8iFFxy4;#B6(oUC(CPqcEZt93IT>t%GHFUB%VS}D8_*|&j~WuDWrdf zAnOgn*Msb`G0If}av~uPqH2JYaH-DJHeOdvL=lD!4N4n3IMeY9(|r`Ur$zgAQIG3UUt*}& zAo97QHneTVBCvZ%8Bo-mgb<9CqlwRjcS1keJ5p^$ka7^U%HUz04Ju;6;|Zsqq8_I*(R`%RPjrb1_*&H!Lh?<(V;m zc6u@POnHt^zBkdbiTf46{ai6IK!st`dW3WND}A zyndO166>Z;KazX=5B&}pjNw|har-|nA z7tczbl7o7dfraXs6C?MIYC#5(Uv*fO${0fc6Q_l)LQhs033ZXmctsG4zn{!zs9`Hb zE%n;XrV@(?6U-H~cnuc}6WPYgmw1>7D~Dn)7HWFrMjHHr|`DwP3zd#fo6E znYF+*#!{KIHOgM#G;Ww`S-}matk*2Oaqa>KIE)Z7j=5w^Q_gqXau6a1;H8%p*#)BD zwE^tvdlNJccEMg2ptFlC8}+<1_?yJ;Z$_vPIES!HDbA>(1=8T3SAwm#2%_#@TmF3s zOk6K__Y&aqrwZ`-qxgN`|HVJ-iHl!ol%{wWJ+i;FL0#hwOWUbhx6=4tDB3=HzYH=I z6b&E{0t|*Zr7Gv0xz;tvovcnAKLxGNW!`}Ed8_mbvR7?yR-aix_pxHnSp~F*+47L_ z6I!Lb4ceX)XUJcvA_kV0TW_jaAJP-k*(KWHcI*8tP?<7n#?C(mi?OMK>WyE|*aKr) zBLj#Y^y+MxTuv2)$RW|BxnEK@K_|AEi>x2)%ZGMRv1WGt6)IGwsE~8&u9wfz-;7^4 zBV`M{WMQ8#?+6B$RW#LP8FCc*f<6)#!V)|J-}*H#k0%6t=u@Qip0-v%!plm9&Gf1D z-c2OJb(b}MtHvY^9Ko^2a9*p11t&VANCeuV_*p*B46xuba{?6*@xuiZ!vYrwvl^3* zMx{pZ-27NrpUQ$*8lTFN7@VDbd)0YA?)%k8kiR#9z&PsG9-#W&p#Np`I(~fvOB;P5 zV;fsLd3&87P4xYXyGO}f9w18MVNq#iU1cN!8(TXk;=`*2$ydY+4~-Ck7-$~DI#(yD zGC8d`J8xF_F7s99W9LY}8Nn1x%2EdLk)nl@(rVDu9pvA zjxFh)Ty}U;?#mG2|R92BQ+k40!p7wR|r) zPb@=#WLQcFd@cJKb{)p;;qez2JAZ9zL$z3i9y!M%wL*<)dDSW<`OxJQ3!^&4qEb~1 ze!4w>3p$2kX_u}y!t7hitQrO;$$W!JO_*I6+H)pTVoCPGG>QX=gNgbzjU{T032dQJ z8AI?|<44JHwR!6HO=ILN?u_JE{+X)tg=%G{pvmXN7>9cSQkdj;yiEa<&Zz!;ljL)S z`rCN(jmB1PBlMrcmQ|{aqRUbTmO#EhuqY~qiWR<9Z-PlCgcv9ep4HL!&2EaUX(z#o1n|XgtN-rR6R+la&6zKdGOSh&n*I zMrbi2NZPxPGzrt;bN4YG*GNBkgA0sOj8G?Wt#CV%HJp9S>I!Tvey=N*tq7t8-bR4- zl@iS%eP%YQfwV`*u9kEDensGhH#(~;C4Y++r7BH)jSDv?n?U@&9Nd-jVCZ!D7n8lX zTM^_@0dPt^lwpJVIjPCv7-iQ*NeGxNFrQN`^aHDiG%ta@hdIgEIvJM*Q@gSx@HdA1 zC@FGPc~R8onocWRS_MiqFC6Eo*6+{3_2)KbKi$J!w{=UVbW;&tWI#=Fg@E~FHBa`# zrGL1*xN-?MU;`NTwE}zI`O%?DA9Or24ZAy~FHGu$Y6{?~^LuLcLFi%Sv2^OjxOHL3 z){tOz3D?hE+_Hg>3Afb36`)I(b6=SEcz7LS+#-#3xL<>SKu-i*kWG}{Oi4o?3eff% zV+J5-IX8xP==*>@!G=^ShE%W+ z&v7!E`K$zUynoP-R|#(Qe=dP&&XAN92?un5?+=RO9`jjL2U8B7Shdl){$+{Cl&vt0 zLxxhDRTpY1Jpdck`7FX^H@Zj$$GQFnNMA48&_aV36p-M#~?UO0Xq#^s%D z?exw6%|1qI)R0&gFS7sWT#J!OWFvMMvSVjnP<+O>BJGKqx6rfaLmg+7}DfeubO^05r2E*YpQhUJ! zp^ZP@g0v(|fB~*~)HsDD9PH4*CQlfI1k8e^uLEW2K2R^5F+TG(+)haHy-O`egtv2T zWvz#bD>;R&mBd>%ecEzRaV2WlYXudjfvlh}Z7~L~!4xu{2?FN`XJB{B^eH2IZ2*ax zml}Cgmh|E=bMPISIF;0lm&2A!+IATMqRkjiC1zQ`v)}cx6fA0H&o^{WS30;ynDIvoAxdEJO6K_{zjJoY2&F!n3^k^z3c!OTWpVYL#{;m{vpylrMOMbSkt~x935t&p#!x8%1xu42n?@$Zl_Uz$s&7}#z3`7Tw+WEQzZ2FxWs z;^!7|wn7TT!>KRxhNeU!3ar|Lw{F{cpQ`j{mPUM5%%52F?No8wZ89s^*^&PY7FDiw zoE9v;cFiA_qLuTK!-P%hxhh>Vl<0Go32MW2NGh)s{;G0ua?)Gam3-Tvj}%SysTgKk z5zwEt@yq&KQ)fpfY@t3Y^mB1kj}d#y6w&!}8tt27rKckmJ|an$yLR|t)*o}XT!$tm z#95HTL92QzzC&WYRF{Nybw0>8$`qVa&*MHiTJ;RO-9Ex6Y*z6&^DXHaUM7z-^KnHF zHnPg2v(iWKR$XhO0=ZYAzkqal?l@`~u_2!f$em+A^zhFscPRl^d=MLSdvx?Wppx`Oc?y2U;_Ww$aSM{3U zE85??l~66@6*pkDG5GwCd!D~{tN)m?{>x%xUv5$c{y|C|G6zTuteZ&Rjv+KZibFk zO&o0xZeL&E`wJor2QW_{qKtb7h*a{?`CEy%mwPU1Fj4ZiCwOuJ_X;{$OZx_V1;&LG zp`S{&oZ`nH97~-D)gU(PFLEY{8ZL^=X{{hIEuv7AN7c*DK)0^MRc4uP?xUaHH+v}a zBhjL%2)?3WaEiJu>>TR^J6Fe|3OZHL8i?*rpQy6&5M@;4`h@`;O}MC}Gck;0V;qBimxN_fVd--b#_EM; zcN7ZAPM7&)wdmEs$mZfrLX1h78jWU+iR}Yt4Az@ZaiQ4K8W_0l9Ltqt`C|OyX!_Hw zE#^pQClNp}`-W$0sa?UUJ!>v#o8lpKJ}_QtBMbo;?nC{Q(UfHgVT{Q@X}HflQldWz z6nP3Gk}{CIRqKSoWwPVY_tE}19%;DHm}hC)7sG2v66-5o{}CrSd%?c>Z7r~yFp1#1 zP!|1J7<>8MxF(j-c;>E?f`!7kgaa(3#mY?V(1IwPlh5w_n@1XgioxxyS)9>TssMGN z5TOFG_a;UmJWWh>5-fO$(QG$U?1ULFMkq)Hq<14k%8DseZ6D1FMB0Hv3yCsYURgA! z@NvbBB&sDl*5=77Q!O0J!=&w@Xbm^Be|b>e>m=h7M7!Tq-{Ed|4=jlR$@pD{z5OGCYFgD-ftPSA21l5Y;gBaix5x!&(5BBUC*CWK}LTMZp zy7vTk3Ly1P|8xs1eNDBeaqV?`^N@aW%%}1qGLN9&VZ6Qy!a8yBu%ihZDq3W3Rhjh= zyMBG!^MFHb9=f_pA9RjtC^f@<+>7hEhA>-0M*~)O1Nja)aQ*YT@azjzO$m9UyPUT@ zA7AK}Zoi-Be_n6(j5Z_uQ$i0|$p;QJ{<%SuHa`YW=+|WAAj22yd&C2ZS+g$*T>?61 zdC7Fpf!>+)z>~Ga?`WO~tHB`Qq8S9{yYA*~J4uAoO|1U5z;z3cz>MFDY7nr1)Ni|CkUEs`QtH-y)^|B1P~+AL2IvBX2!}Y`{;a z0XNZ)_wbK=SvzYrXg* zfwGOZ72p6QU^~RX*w7vjHX9H^{?B=rb;mK@1XKwI;0>eyE8~D?wbyfmKSDokPZ5Bg zh1q}0xWztx7bd_T#Tt;!Z)c_cx~jciqW%&6Zz^+t&hho~M&JnmFBKnP3it~U@T~Sq z!uca6;H03Pwwc+V(U#jK0=og_j|Ge+f3MnpfQ{h~-GblJ((ap>hn1wZu?1i&^{0f# z(^l&c#2*v@RBH{OsN{dk=q$q@p?|cRpp(9?{r?3ze~Rid$5H_gKs5uPQvMC~EkIV_ z4;lX6kAGl)%k-Zs;;FdoU(nTF^+JEd{ZXy|ZNzvgDfkl)QSy&?e{1^xCNTK4HlFI$ z{ba!cNa_5cHvV~#cq+s56E0fm|0cX2gYF+EylK(yNU+x6IEU};LsXm2&s^ReyK2ZI) zy!`_E#TIurp)XZ5Q_!BeWI zLE(Q=>FWFw)qe>Q{}lddbn~C^H@g1>|Dz@TDc1Q@s;6O6e^OzY{R^t^mG-}?>uIFP zpCsIt|AOS7<4!&;(bK?uKgnEe{)y~YBlAZtPg$PE zANt86gf2BU@-Y#5d1ny{ka5B-OPRxl%)Me z@YgKyZ#HY6mgK1y$4{a+9*>$4?@*y8l}k{= diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c3f6c760..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.13-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 aeaca8f7..0fce190e 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit aeaca8f76116d05f78792c5610f1d1d4d21a2c60 +Subproject commit 0fce190e8fa7dbcea2eaa768aadb429939b070c1 diff --git a/macrobenchmark/build.gradle.kts b/macrobenchmark/build.gradle.kts index 1c9a6238..bac44a34 100644 --- a/macrobenchmark/build.gradle.kts +++ b/macrobenchmark/build.gradle.kts @@ -21,7 +21,7 @@ android { } 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 2c04b87b..58870569 100644 --- a/playstore/build.gradle.kts +++ b/playstore/build.gradle.kts @@ -15,7 +15,7 @@ android { compileSdk = 36 defaultConfig { - minSdk = 27 + minSdk = 31 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } From 3410350b0dbf587877c1c87ab8ee775e1dcda45f Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Fri, 3 Oct 2025 14:53:03 +0300 Subject: [PATCH 11/15] Migrate extras --- app/src/main/AndroidManifest.xml | 29 ++-- .../com/anod/appwatcher/AppWatcherActivity.kt | 85 ++++++++-- .../anod/appwatcher/MarketSearchActivity.kt | 21 --- .../anod/appwatcher/ShareRecieverActivity.kt | 19 ++- .../appwatcher/installed/InstalledActivity.kt | 112 ------------- .../anod/appwatcher/navigation/SceneNavKey.kt | 4 +- .../search/SearchComposeActivity.kt | 109 ------------- .../appwatcher/search/SearchResultsScreen.kt | 16 +- .../anod/appwatcher/sync/SyncNotification.kt | 5 +- .../tags/TagWatchListComposeActivity.kt | 152 ------------------ .../anod/appwatcher/watchlist/MainActivity.kt | 44 +---- .../anod/appwatcher/watchlist/MainScreen.kt | 3 +- .../watchlist/WatchListStateViewModel.kt | 35 ++-- 13 files changed, 132 insertions(+), 502 deletions(-) delete mode 100644 app/src/main/java/com/anod/appwatcher/MarketSearchActivity.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99e7761f..531fa3c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,28 +55,25 @@ + - + - - - @@ -117,7 +108,7 @@ android:theme="@style/Theme.AppCompat.DayNight" /> diff --git a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt index 3a8eb4ba..f6ed4bf9 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -25,13 +25,14 @@ 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.tags.TagWatchListScreenScene 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.watchlist.WatchListStateViewModel import com.anod.appwatcher.wishlist.WishListScreenScene +import info.anodsplace.framework.app.addMultiWindowFlags import org.koin.core.component.KoinComponent @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -62,18 +63,40 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { 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("open_recently_installed")) { intent!!.extras!!.remove("open_recently_installed") elements += SceneNavKey.Installed(importMode = false) - } else if (extras.containsKey(WatchListStateViewModel.EXTRA_TAG_ID)) { - val extraTagId = extras.getInt(WatchListStateViewModel.EXTRA_TAG_ID) - intent!!.extras!!.remove(WatchListStateViewModel.EXTRA_TAG_ID) + } 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(WatchListStateViewModel.EXTRA_TAG_COLOR) + color = extras.getInt(EXTRA_LIST_TAG_COLOR) ) ) } @@ -112,10 +135,10 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { EmptyBoxSmile() } ) - ) { + ) { key -> val wideLayout by foldableDevice.layout.collectAsState() SearchResultsScreenScene( - wideLayout = wideLayout, + initialState = key.toViewState(wideLayout), navigateBack = { backStack.removeLastOrNull() }, ) } @@ -172,12 +195,54 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { } 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 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 = "com.anod.appwatcher://tags/$tagId?color=$initialColor".toUri() - putExtra(WatchListStateViewModel.EXTRA_TAG_ID, tagId) - putExtra(WatchListStateViewModel.EXTRA_TAG_COLOR, initialColor) + 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) } + + 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/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/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/installed/InstalledActivity.kt b/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt deleted file mode 100644 index fa45edd3..00000000 --- a/app/src/main/java/com/anod/appwatcher/installed/InstalledActivity.kt +++ /dev/null @@ -1,112 +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.utils.onScreenCommonAction -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 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)) }, - ) - } - ) - } 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)) }, - ) - } - } - } - } - - lifecycleScope.launch { - viewModel.viewActions.collect { onScreenCommonAction(it, { finish() } ) } - } - - 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/navigation/SceneNavKey.kt b/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt index 313628b7..8ec2fdb1 100644 --- a/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt +++ b/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt @@ -13,7 +13,9 @@ sealed interface SceneNavKey : NavKey { data class Search( val keyword: String = "", val focus: Boolean = false, - val initiateSearch: Boolean = false + val initiateSearch: Boolean = false, + val isPackageSearch: Boolean = false, + val isShareSource: Boolean = false ) : SceneNavKey @Serializable 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 d0414530..00000000 --- a/app/src/main/java/com/anod/appwatcher/search/SearchComposeActivity.kt +++ /dev/null @@ -1,109 +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 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, - pagingDataFlow = { viewModel.pagingData }, - onEvent = viewModel::handleEvent, - viewActions = viewModel.viewActions, - onShowAccountDialog = { accountSelectionDialog.show() }, - ) - }, - detail = { - DetailContent( - app = screenState.selectedApp, - onDismissRequest = { viewModel.handleEvent(SearchViewEvent.SelectApp(app = null)) }, - ) - } - ) - } else { - SearchResultsScreen( - screenState = screenState, - pagingDataFlow = { viewModel.pagingData }, - onEvent = viewModel::handleEvent, - viewActions = viewModel.viewActions, - onShowAccountDialog = { accountSelectionDialog.show() }, - ) - if (screenState.selectedApp != null) { - DetailsDialog( - app = screenState.selectedApp!!, - onDismissRequest = { viewModel.handleEvent(SearchViewEvent.SelectApp(app = null)) }, - ) - } - } - } - } - - 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 621ab6ee..43a3999a 100644 --- a/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/search/SearchResultsScreen.kt @@ -45,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 @@ -60,11 +61,18 @@ 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(wideLayout: FoldableDeviceLayout, navigateBack: () -> Unit = {}) { - val viewModel: SearchViewModel = viewModel(factory = SearchViewModel.Factory( - initialState = SearchViewState(wideLayout = wideLayout), // intentToState(intent, foldableDevice.layout.value), - )) +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, 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..84b2173f 100644 --- a/app/src/main/java/com/anod/appwatcher/sync/SyncNotification.kt +++ b/app/src/main/java/com/anod/appwatcher/sync/SyncNotification.kt @@ -14,6 +14,7 @@ import com.anod.appwatcher.utils.color.DynamicColors import com.anod.appwatcher.watchlist.MainActivity import info.anodsplace.context.ApplicationContext import info.anodsplace.framework.text.Html +import androidx.core.net.toUri /** * @author alex @@ -92,9 +93,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/TagWatchListComposeActivity.kt b/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt deleted file mode 100644 index 79fe6cdd..00000000 --- a/app/src/main/java/com/anod/appwatcher/tags/TagWatchListComposeActivity.kt +++ /dev/null @@ -1,152 +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.model.Filters -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.WatchListStateViewModel -import info.anodsplace.framework.app.addMultiWindowFlags -import info.anodsplace.framework.content.showToast -import info.anodsplace.framework.content.startActivity -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 { - when (it) { - is WatchListAction.SelectApp -> {} - is WatchListAction.ShowToast -> showToast(it) - is WatchListAction.StartActivity -> startActivity(it) - WatchListAction.NavigateBack -> finish() - is WatchListAction.NavigateTo -> {} - } - - } - } - - 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/watchlist/MainActivity.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt index 32b5eaca..ffe4758d 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt @@ -16,8 +16,8 @@ import androidx.core.os.bundleOf import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.anod.appwatcher.AppWatcherActivity 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 @@ -27,7 +27,6 @@ import com.anod.appwatcher.compose.BaseComposeActivity import com.anod.appwatcher.compose.MainDetailScreen import com.anod.appwatcher.database.entities.Tag import com.anod.appwatcher.history.HistoryListActivity -import com.anod.appwatcher.tags.TagWatchListComposeActivity import com.anod.appwatcher.utils.getIntentFlags import com.anod.appwatcher.utils.prefs import info.anodsplace.applog.AppLog @@ -45,7 +44,8 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { WatchListStateViewModel.Factory( defaultFilterId = prefs.defaultMainFilterId, wideLayout = foldableDevice.layout.value, - collectRecentlyInstalledApps = prefs.showRecent + collectRecentlyInstalledApps = prefs.showRecent, + initialTag = Tag.empty ) }) private lateinit var accountSelectionDialog: AccountSelectionDialog @@ -54,31 +54,6 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { 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 @@ -209,7 +184,7 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { when (action) { is MainViewAction.NavigateTo -> { when (action.id) { - DrawerItem.Id.Add -> startActivity(Intent(this, MarketSearchActivity::class.java)) + DrawerItem.Id.Add -> startActivity(AppWatcherActivity.searchIntent(this, "", focus = true)) DrawerItem.Id.Installed -> { }//startActivity(InstalledActivity.intent(false, this)) DrawerItem.Id.Refresh -> { } DrawerItem.Id.Settings -> startActivity(Intent(this, SettingsActivity::class.java)) @@ -217,7 +192,7 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { DrawerItem.Id.Purchases -> startActivity(HistoryListActivity.intent(this)) } } - is MainViewAction.NavigateToTag -> startActivity(TagWatchListComposeActivity.createTagIntent(action.tag, this)) + is MainViewAction.NavigateToTag -> { } MainViewAction.RequestNotificationPermission -> notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) MainViewAction.ChooseAccount -> accountSelectionDialog.show() is MainViewAction.DrawerState -> { } @@ -226,13 +201,4 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { } } - 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 474618db..b1384b0f 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainScreen.kt @@ -38,7 +38,8 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, naviga WatchListStateViewModel.Factory( defaultFilterId = prefs.defaultMainFilterId, wideLayout = wideLayout, - collectRecentlyInstalledApps = prefs.showRecent + collectRecentlyInstalledApps = prefs.showRecent, + initialTag = Tag.empty ), key = SceneNavKey.Main.toString() ) diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt index 6a566d46..bee13a5f 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt @@ -130,7 +130,7 @@ private fun showToastAction(resId: Int = 0, text: String = "", length: Int = Toa class WatchListStateViewModel( state: SavedStateHandle, - initialTag: Tag, + tag: Tag, defaultFilterId: Int, collectRecentlyInstalledApps: Boolean, wideLayout: FoldableDeviceLayout @@ -145,23 +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, - private val initialTag: Tag = Tag.empty + private val initialTag: Tag ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: KClass, extras: CreationExtras): T { + val state = extras.createSavedStateHandle() return WatchListStateViewModel( - state = extras.createSavedStateHandle(), - initialTag = initialTag, + state = state, + tag = initialTag, defaultFilterId = defaultFilterId, wideLayout = wideLayout, collectRecentlyInstalledApps = collectRecentlyInstalledApps @@ -173,15 +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 initialTag - } else { - extraTag - } viewState = WatchListSharedState( tag = tag, sortId = prefs.sortIndex, @@ -200,6 +186,11 @@ class WatchListStateViewModel( } } + if (viewState.tag.isEmpty) { + AppLog.d("mark updates as viewed.") + prefs.isLastUpdatesViewed = true + } + if (!viewState.tag.isEmpty) { viewModelScope.launch { db.tags() @@ -273,7 +264,7 @@ class WatchListStateViewModel( val query = viewState.titleFilter viewState = viewState.copy(showSearch = false, titleFilter = "") emitAction(WatchListAction.NavigateTo( - SceneNavKey.Search(keyword = query, focus = true) + SceneNavKey.Search(keyword = query, focus = true, initiateSearch = true) )) } @@ -298,7 +289,7 @@ class WatchListStateViewModel( is WatchListEvent.EmptyButton -> { when (event.idx) { 1 -> emitAction(WatchListAction.NavigateTo( - SceneNavKey.Search(focus = true) + SceneNavKey.Search(focus = true,) )) 2 -> emitAction(WatchListAction.NavigateTo(SceneNavKey.Installed(importMode = true))) 3 -> emitAction(startActivityAction( @@ -320,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() From 654077baca18e747b69ad3958e9f1a93a17aa647 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Sat, 25 Oct 2025 13:43:35 +0300 Subject: [PATCH 12/15] remove additional activities --- app/build.gradle.kts | 12 +- app/src/main/AndroidManifest.xml | 27 +-- .../com/anod/appwatcher/AppWatcherActivity.kt | 78 +++++++- .../com/anod/appwatcher/SettingsActivity.kt | 3 - .../appwatcher/accounts/AuthTokenBlocking.kt | 8 +- .../appwatcher/backup/gdrive/GDriveSignIn.kt | 38 ++-- .../appwatcher/backup/gdrive/GDriveSync.kt | 7 +- .../appwatcher/backup/gdrive/GDriveUpload.kt | 4 +- .../appwatcher/backup/gdrive/UploadService.kt | 9 +- .../appwatcher/compose/BaseComposeActivity.kt | 1 - .../appwatcher/compose/MainDetailsScreen.kt | 53 ----- .../java/com/anod/appwatcher/compose/Theme.kt | 4 - .../appwatcher/compose/ViewModelStoreOwner.kt | 2 +- .../anod/appwatcher/details/DetailsPanel.kt | 10 +- .../appwatcher/history/HistoryListActivity.kt | 97 ---------- .../appwatcher/history/HistoryListScreen.kt | 22 +++ .../history/HistoryListViewModel.kt | 10 +- .../installed/InstalledListScreen.kt | 10 +- .../installed/InstalledListViewModel.kt | 23 +-- .../anod/appwatcher/navigation/SceneNavKey.kt | 6 + .../preferences/SettingsActivity.kt | 183 ------------------ .../appwatcher/preferences/SettingsScreen.kt | 10 +- .../preferences/SettingsViewModel.kt | 41 ++-- ...yActivity.kt => SchedulesHistoryScreen.kt} | 46 ++--- .../sync/SchedulesHistoryViewModel.kt | 46 +++++ .../com/anod/appwatcher/tags/AppsTagDialog.kt | 4 +- .../com/anod/appwatcher/tags/EditTagDialog.kt | 4 +- .../appwatcher/tags/TagSelectionDialog.kt | 6 +- .../appwatcher/tags/TagsSelectionViewModel.kt | 2 +- .../appwatcher/userLog/UserLogActivity.kt | 36 ---- .../anod/appwatcher/userLog/UserLogScreen.kt | 24 ++- .../appwatcher/userLog/UserLogViewModel.kt | 2 +- .../appwatcher/utils/ScreenCommonAction.kt | 24 --- .../anod/appwatcher/watchlist/MainActivity.kt | 52 +---- .../watchlist/WatchListStateViewModel.kt | 2 +- .../appwatcher/wishlist/WishListActivity.kt | 88 --------- build.gradle.kts | 3 +- gradle/libs.versions.toml | 36 ++-- lib | 2 +- 39 files changed, 309 insertions(+), 726 deletions(-) delete mode 100644 app/src/main/java/com/anod/appwatcher/SettingsActivity.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/compose/MainDetailsScreen.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/history/HistoryListActivity.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt rename app/src/main/java/com/anod/appwatcher/sync/{SchedulesHistoryActivity.kt => SchedulesHistoryScreen.kt} (86%) create mode 100644 app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryViewModel.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/userLog/UserLogActivity.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/utils/ScreenCommonAction.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bfac4751..01cdcef6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,13 +1,15 @@ 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) - alias(libs.plugins.jetbrains.kotlin.serialization) id("kotlin-parcelize") id("com.google.android.gms.oss-licenses-plugin") } @@ -53,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") } @@ -101,6 +103,7 @@ 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) @@ -113,8 +116,9 @@ dependencies { 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 531fa3c8..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 f6ed4bf9..c99bf065 100644 --- a/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/AppWatcherActivity.kt @@ -4,6 +4,10 @@ import android.content.Context import android.content.Intent import android.os.Bundle 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 @@ -21,12 +25,15 @@ 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 @@ -55,7 +62,37 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { backStack = backStack, onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, sceneStrategy = listDetailStrategy, - entryProvider = provideNavEntries(backStack) + 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) + ) + } ) } } @@ -86,8 +123,8 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { )) } var elements = arrayOf(SceneNavKey.Main) - if (extras.containsKey("open_recently_installed")) { - intent!!.extras!!.remove("open_recently_installed") + 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) @@ -99,6 +136,9 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { 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 } @@ -146,6 +186,30 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { 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() } ) } @@ -208,6 +272,9 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { 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" @@ -237,6 +304,11 @@ class AppWatcherActivity : BaseComposeActivity(), KoinComponent { 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) 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/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 82716def..3f9f0ab4 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,12 +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.suspendCoroutine -import org.koin.java.KoinJavaComponent import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine internal fun createGDriveSignInOptions(): GoogleSignInOptions { return GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) @@ -37,10 +35,10 @@ 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) } class GDriveSignIn(private val context: ApplicationContext) { @@ -62,6 +60,10 @@ class GDriveSignIn(private val context: ApplicationContext) { val notificationManager = KoinJavaComponent.getKoin().get() notificationManager.notify(SyncNotification.GMS_NOTIFICATION_ID, notification) } + + fun getLastSignedInAccount(context: Context): Account? { + return GoogleSignIn.getLastSignedInAccount(context)?.account + } } class GoogleSignInRequestException(val intent: Intent, val resultCode: Int) : Throwable() @@ -69,13 +71,13 @@ class GDriveSignIn(private val context: ApplicationContext) { suspend fun signIn() = suspendCoroutine { continuation -> driveConnect.connect(object : GoogleSignInConnect.Result { - override fun onSuccess(account: GoogleSignInAccount, client: GoogleSignInClient) { + 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") - continuation.resumeWithException(GoogleSignInRequestException(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)) } }) } @@ -88,10 +90,6 @@ class GDriveSignIn(private val context: ApplicationContext) { }) } - fun requestEmail(lastSignedAccount: GoogleSignInAccount) { - // GoogleSignIn.requestPermissions(activity, RESULT_CODE_GDRIVE_SIGN_IN, lastSignedAccount, Scope("email")) - } - suspend fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = suspendCoroutine { continuation -> // Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...); if (resultCode == Activity.RESULT_OK && data?.extras != null) { @@ -119,10 +117,10 @@ 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 { @@ -131,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 09bb20be..9dc54fa2 100644 --- a/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/compose/BaseComposeActivity.kt @@ -1,6 +1,5 @@ package com.anod.appwatcher.compose -import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.enableEdgeToEdge 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 7051af40..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 = {}) }, - detail = { DetailContent(app = null, onDismissRequest = {}) }, - ) - } -} \ No newline at end of file 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 d7d3acef..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 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/details/DetailsPanel.kt b/app/src/main/java/com/anod/appwatcher/details/DetailsPanel.kt index e77b64ee..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 @@ -116,10 +116,10 @@ import info.anodsplace.compose.toAnnotatedString 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 @@ -128,7 +128,7 @@ private val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.M @Composable fun DetailsPanel(app: App, onDismissRequest: () -> Unit) { - val storeOwner = rememberViwModeStoreOwner() + val storeOwner = rememberViewModeStoreOwner() val isSystemInDarkTheme = isSystemInDarkTheme() val viewModel: DetailsViewModel = viewModel( key = "details-${app.appId}-${app.rowId}", @@ -157,7 +157,7 @@ fun DetailsPanel(app: App, onDismissRequest: () -> Unit) { @Composable fun DetailsDialog(app: App, onDismissRequest: () -> Unit) { - val storeOwner = rememberViwModeStoreOwner() + val storeOwner = rememberViewModeStoreOwner() val isSystemInDarkTheme = isSystemInDarkTheme() val viewModel: DetailsViewModel = viewModel( key = "details-${app.appId}-${app.rowId}", 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 ff54f04a..00000000 --- a/app/src/main/java/com/anod/appwatcher/history/HistoryListActivity.kt +++ /dev/null @@ -1,97 +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 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 - ) - }, - detail = { - DetailContent( - app = screenState.selectedApp, - onDismissRequest = { viewModel.handleEvent(HistoryListEvent.SelectApp(app = null)) }, - ) - } - ) - } else { - HistoryListScreen( - screenState = screenState, - onEvent = viewModel::handleEvent, - pagingDataFlow = viewModel.pagingData, - viewActions = viewModel.viewActions, - ) - if (screenState.selectedApp != null) { - DetailsDialog( - app = screenState.selectedApp!!, - onDismissRequest = { viewModel.handleEvent(HistoryListEvent.SelectApp(app = null)) }, - ) - } - } - } - } - - 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 e4371261..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,15 +40,35 @@ 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.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, 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 b94b4002..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,12 +12,15 @@ 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 @@ -29,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 @@ -68,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, ) } @@ -101,6 +104,7 @@ class HistoryListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel ) } .flow + .onStart { authToken.checkToken(prefs.account?.toAndroidAccount()) } .cachedIn(viewModelScope) .combine( flow = viewStates.map { it.nameFilter }.distinctUntilChanged(), 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 a99f2b03..9a1b4257 100644 --- a/app/src/main/java/com/anod/appwatcher/installed/InstalledListScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/installed/InstalledListScreen.kt @@ -26,13 +26,11 @@ 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.utils.ScreenCommonAction 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.showToast -import info.anodsplace.framework.content.startActivity +import info.anodsplace.framework.content.onScreenCommonAction @Composable fun InstalledListScreenScene( @@ -66,11 +64,7 @@ fun InstalledListScreenScene( LaunchedEffect(true) { viewModel.viewActions.collect { action -> - when (action) { - ScreenCommonAction.NavigateBack -> navigateBack() - is ScreenCommonAction.ShowToast -> context.showToast(action) - is ScreenCommonAction.StartActivity -> context.startActivity(action) - } + context.onScreenCommonAction(action, navigateBack) } } } 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 7a1f91b1..31eea1cd 100644 --- a/app/src/main/java/com/anod/appwatcher/installed/InstalledListViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/installed/InstalledListViewModel.kt @@ -18,7 +18,6 @@ import com.anod.appwatcher.accounts.toAndroidAccount import com.anod.appwatcher.database.entities.App import com.anod.appwatcher.utils.BaseFlowViewModel import com.anod.appwatcher.utils.PackageChangedReceiver -import com.anod.appwatcher.utils.ScreenCommonAction import com.anod.appwatcher.utils.SelectionState import com.anod.appwatcher.utils.filterWithExtra import com.anod.appwatcher.utils.getInt @@ -27,6 +26,7 @@ import com.anod.appwatcher.utils.prefs import com.anod.appwatcher.watchlist.WatchListEvent import info.anodsplace.framework.app.FoldableDeviceLayout 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 @@ -60,7 +60,6 @@ sealed interface InstalledListEvent { class SelectApp(val app: App?) : InstalledListEvent class AuthTokenError(val error: CheckTokenError) : InstalledListEvent object Import : InstalledListEvent - object NoAccount : InstalledListEvent } class InstalledListViewModel( @@ -136,8 +135,6 @@ class InstalledListViewModel( tokenErrorToast() } } - - InstalledListEvent.NoAccount -> tokenErrorToast() } } @@ -205,19 +202,13 @@ class InstalledListViewModel( } private suspend fun checkAuthToken(): Boolean { - val account = prefs.account?.toAndroidAccount() - return if (account == null) { - handleEvent(InstalledListEvent.NoAccount) - false - } else { - when (val result = authToken.checkToken(account)) { - is CheckTokenResult.Error -> { - 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 index 8ec2fdb1..d896242a 100644 --- a/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt +++ b/app/src/main/java/com/anod/appwatcher/navigation/SceneNavKey.kt @@ -21,6 +21,12 @@ sealed interface SceneNavKey : NavKey { @Serializable data object Settings : SceneNavKey + @Serializable + data object RefreshHistory : SceneNavKey + + @Serializable + data object UserLog : SceneNavKey + @Serializable data class Installed(val importMode: Boolean) : 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 26a320ad..00000000 --- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsActivity.kt +++ /dev/null @@ -1,183 +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.compose.AppTheme -import com.anod.appwatcher.compose.BaseComposeActivity -import com.anod.appwatcher.compose.MainDetailScreen -import com.anod.appwatcher.utils.prefs -import com.jakewharton.processphoenix.ProcessPhoenix -import info.anodsplace.applog.AppLog -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 kotlinx.coroutines.launch - -@SuppressLint("Registered") -open class SettingsActivity : BaseComposeActivity() { - - private lateinit var gDriveErrorIntentRequest: ActivityResultLauncher - private lateinit var notificationPermissionRequest: ActivityResultLauncher - 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()) { activityResult -> - viewModel.handleEvent(SettingsViewEvent.GDriveActivityResult(activityResult)) - } - 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, this@SettingsActivity) - } - } - } - - 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, activity: SettingsActivity) { - when (action) { - is SettingsViewAction.ExportResult -> onExportResult(action.result) - is SettingsViewAction.ImportResult -> onImportResult(action.result) - 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()) - SettingsViewAction.OnBackPressed -> activity.finish() - is SettingsViewAction.ShowToast -> activity.showToast(action) - is SettingsViewAction.StartActivity -> activity.startActivity(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() - } - } - - private fun recreateWatchlist() { - val i = Intent(this@SettingsActivity, AppWatcherActivity::class.java) - i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - startActivity(i) - } -} \ 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 6220552a..45df8a43 100644 --- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsScreen.kt +++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsScreen.kt @@ -37,14 +37,13 @@ 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.lifecycleScope 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.backup.gdrive.GDriveSignIn import com.anod.appwatcher.compose.AppTheme import com.jakewharton.processphoenix.ProcessPhoenix import info.anodsplace.applog.AppLog @@ -63,7 +62,7 @@ import info.anodsplace.permissions.toRequestInput import org.koin.java.KoinJavaComponent @Composable -fun SettingsScreenScene(navigateBack: () -> Unit) { +fun SettingsScreenScene(navigateBack: () -> Unit, navigateTo: (NavKey) -> Unit) { val viewModel: SettingsViewModel = viewModel() val screenState by viewModel.viewStates.collectAsState(initial = viewModel.viewState) val context = LocalContext.current @@ -96,7 +95,8 @@ fun SettingsScreenScene(navigateBack: () -> Unit) { ProcessPhoenix.triggerRebirth(context.applicationContext, Intent(context.applicationContext, AppWatcherActivity::class.java)) } SettingsViewAction.RequestNotificationPermission -> notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) - SettingsViewAction.OnBackPressed -> navigateBack() + SettingsViewAction.NavigateBack -> navigateBack() + is SettingsViewAction.NavigateTo -> navigateTo(action.navKey) is SettingsViewAction.ShowToast -> context.showToast(action) is SettingsViewAction.StartActivity -> context.startActivity(action) } @@ -159,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 70677e2e..154de825 100644 --- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt @@ -10,6 +10,7 @@ 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 @@ -19,14 +20,12 @@ 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 @@ -68,7 +67,7 @@ sealed interface 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 @@ -82,7 +81,7 @@ sealed interface SettingsViewEvent { } sealed interface SettingsViewAction { - data object OnBackPressed : 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 @@ -91,12 +90,7 @@ sealed interface SettingsViewAction { object RequestNotificationPermission : SettingsViewAction class ExportResult(val result: Int) : SettingsViewAction class ImportResult(val result: Int) : SettingsViewAction -} - -private fun startActivityAction(intent: Intent): SettingsViewAction { - return SettingsViewAction.StartActivity( - intent = intent, - ) + data class NavigateTo(val navKey: NavKey) : SettingsViewAction } private fun showToastAction(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT): SettingsViewAction { @@ -143,14 +137,20 @@ class SettingsViewModel : BaseFlowViewModel gDriveSyncNow() is SettingsViewEvent.GDriveSyncToggle -> gDriveSyncToggle(event.checked) - SettingsViewEvent.OnBackNav -> emitAction(SettingsViewAction.OnBackPressed) - SettingsViewEvent.OpenRefreshHistory -> emitAction(startActivityAction( - Intent(application, SchedulesHistoryActivity::class.java), - )) - SettingsViewEvent.OpenUserLog -> emitAction(startActivityAction( - Intent(application, UserLogActivity::class.java), + 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), )) is SettingsViewEvent.SetRecreateFlag -> { @@ -172,7 +172,8 @@ class SettingsViewModel : BaseFlowViewModel emitAction(startActivityAction( + SettingsViewEvent.ShowAppSettings -> emitAction( + SettingsViewAction.StartActivity( intent = Intent().forAppInfo(application.packageName), )) SettingsViewEvent.CheckNotificationPermission -> { @@ -256,7 +257,7 @@ class SettingsViewModel : BaseFlowViewModel 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) } } } 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/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/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/TagsSelectionViewModel.kt b/app/src/main/java/com/anod/appwatcher/tags/TagsSelectionViewModel.kt index e59c78cb..a9b90139 100644 --- a/app/src/main/java/com/anod/appwatcher/tags/TagsSelectionViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/tags/TagsSelectionViewModel.kt @@ -12,8 +12,8 @@ import com.anod.appwatcher.database.AppTagsTable import com.anod.appwatcher.database.AppsDatabase import com.anod.appwatcher.database.entities.Tag import com.anod.appwatcher.utils.BaseFlowViewModel -import com.anod.appwatcher.utils.ScreenCommonAction import info.anodsplace.compose.CheckBoxItem +import info.anodsplace.framework.content.ScreenCommonAction import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map 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 dc80a439..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 com.anod.appwatcher.utils.onScreenCommonAction -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 { onScreenCommonAction(action = it, navigateBack = { finish() }) } - } - } -} \ 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 eef8d124..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 com.anod.appwatcher.utils.ScreenCommonAction +import info.anodsplace.framework.content.ScreenCommonAction import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList diff --git a/app/src/main/java/com/anod/appwatcher/utils/ScreenCommonAction.kt b/app/src/main/java/com/anod/appwatcher/utils/ScreenCommonAction.kt deleted file mode 100644 index 4a66e225..00000000 --- a/app/src/main/java/com/anod/appwatcher/utils/ScreenCommonAction.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.anod.appwatcher.utils - -import android.content.Context -import android.content.Intent -import android.widget.Toast -import androidx.annotation.StringRes -import info.anodsplace.framework.content.ShowToastActionDefaults -import info.anodsplace.framework.content.StartActivityAction -import info.anodsplace.framework.content.showToast -import info.anodsplace.framework.content.startActivity - -sealed interface ScreenCommonAction { - data class StartActivity(override val intent: Intent) : ScreenCommonAction, StartActivityAction - class ShowToast(@StringRes resId: Int = 0, text: String = "", length: Int = Toast.LENGTH_SHORT) : ShowToastActionDefaults(resId, text, length), ScreenCommonAction - object NavigateBack : ScreenCommonAction -} - -fun Context.onScreenCommonAction(action: ScreenCommonAction, navigateBack: () -> Unit) { - when (action) { - ScreenCommonAction.NavigateBack -> navigateBack() - is ScreenCommonAction.ShowToast -> showToast(action) - is ScreenCommonAction.StartActivity -> startActivity(action) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt index ffe4758d..17750915 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt @@ -1,6 +1,5 @@ package com.anod.appwatcher.watchlist -import android.content.Intent import android.os.Bundle import android.widget.Toast import androidx.activity.OnBackPressedCallback @@ -12,22 +11,16 @@ 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.AppWatcherActivity -import com.anod.appwatcher.BuildConfig 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.history.HistoryListActivity -import com.anod.appwatcher.utils.getIntentFlags import com.anod.appwatcher.utils.prefs import info.anodsplace.applog.AppLog import info.anodsplace.framework.content.showToast @@ -103,47 +96,6 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { } } } - - 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) } -// ) -// } - } } } @@ -187,9 +139,9 @@ abstract class MainActivity : BaseComposeActivity(), KoinComponent { DrawerItem.Id.Add -> startActivity(AppWatcherActivity.searchIntent(this, "", focus = true)) DrawerItem.Id.Installed -> { }//startActivity(InstalledActivity.intent(false, this)) DrawerItem.Id.Refresh -> { } - DrawerItem.Id.Settings -> startActivity(Intent(this, SettingsActivity::class.java)) + DrawerItem.Id.Settings -> {} //startActivity(Intent(this, SettingsActivity::class.java)) DrawerItem.Id.Wishlist -> {} //startActivity(WishListActivity.intent(this)) - DrawerItem.Id.Purchases -> startActivity(HistoryListActivity.intent(this)) + DrawerItem.Id.Purchases -> {} //startActivity(HistoryListActivity.intent(this)) } } is MainViewAction.NavigateToTag -> { } diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt index bee13a5f..6515d0be 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListStateViewModel.kt @@ -114,7 +114,7 @@ sealed interface WatchListAction { data class StartActivity(override val intent: Intent) : WatchListAction, StartActivityAction class ShowToast(resId: Int, text: String, length: Int) : ShowToastActionDefaults(resId, text, length), WatchListAction data class SelectApp(val app: App) : WatchListAction - class NavigateTo(val navKey: NavKey): WatchListAction + data class NavigateTo(val navKey: NavKey): WatchListAction data object NavigateBack : WatchListAction } 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 ade732ec..00000000 --- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListActivity.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.anod.appwatcher.wishlist - -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 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, - ) - }, - detail = { - DetailContent( - app = screenState.selectedApp, - onDismissRequest = { viewModel.handleEvent(WishListEvent.SelectApp(app = null)) }, - ) - } - ) - } else { - WishListScreen( - screenState = screenState, - onEvent = viewModel::handleEvent, - pagingDataFlow = viewModel.pagingData, - viewActions = viewModel.viewActions, - ) - if (screenState.selectedApp != null) { - DetailsDialog( - app = screenState.selectedApp!!, - onDismissRequest = { viewModel.handleEvent(WishListEvent.SelectApp(app = null)) }, - ) - } - } - } - } - - lifecycleScope.launch { - foldableDevice.layout.collect { - viewModel.handleEvent(WishListEvent.SetWideLayout(it)) - } - } - } - -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index ac844d66..0fe82984 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,5 +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.jetbrains.kotlin.serialization) 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 4393decf..78ee7370 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,29 +2,29 @@ activity-compose = "1.11.0" agp = "8.13.0" annotation = "1.9.1" -ksp = "2.2.20-2.0.3" +ksp = "2.3.0" androidx-junit = "1.3.0" appcompat = "1.7.1" -benchmark-macro-junit4 = "1.4.1" +benchmark = "1.4.1" coil = "3.3.0" -compose-bom = "2025.09.01" +compose-bom = "2025.10.01" compose-material3 = "1.4.0" core-splashscreen = "1.0.1" core-ktx = "1.17.0" espresso-core = "3.7.0" -firebase-crashlytics = "20.0.2" +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.3" +google-services = "4.4.4" junit = "4.13.2" koin-core = "4.1.1" -kotlin = "2.2.20" +kotlin = "2.2.21" kotlinx-collections-immutable = "0.4.0" kotlinx-datetime = "0.7.1" kotlinx-serialization = "1.9.0" -ktor = "3.3.0" +ktor = "3.3.1" ktlint-gradle = "13.1.0" ktlint-compose = "0.4.27" coroutines = "1.10.2" @@ -32,8 +32,7 @@ leakcanary-android = "2.14" lifecycle = "2.9.4" navigation3-ui = "1.0.0-SNAPSHOT" navigation3-adaptive = "1.0.0-SNAPSHOT" -navigation-compose = "2.9.5" -okhttp = "5.1.0" +okhttp = "5.2.1" oss-licenses-plugin = "0.10.9" paging = "3.3.6" palette = "1.0.0" @@ -41,18 +40,17 @@ play-services-auth = "21.4.0" play-services-identity = "18.1.0" play-services-oss-licenses = "17.3.0" process-phoenix = "3.0.0" -room = "2.8.1" -runtime-tracing = "1.9.2" +room = "2.8.3" +runtime-tracing = "1.9.4" uiautomator = "2.3.0" window = "1.5.0" -work-runtime = "2.10.5" +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" } @@ -90,13 +88,12 @@ 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" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktlint-compose = { group = "io.nlopez.compose.rules", name = "ktlint", version.ref = "ktlint-compose" } -kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary-android" } @@ -104,8 +101,6 @@ lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } -navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } - okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } oss-licenses-plugin = { module = "com.google.android.gms:oss-licenses-plugin", version.ref = "oss-licenses-plugin" } @@ -129,11 +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" } -jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", 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/lib b/lib index 0fce190e..3da0a8f5 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit 0fce190e8fa7dbcea2eaa768aadb429939b070c1 +Subproject commit 3da0a8f5aa63d3dd3b1739b35c2ac1a627a6e46b From 97a5163a6b5606454d85dadaf12541018d6ba8c8 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Sat, 25 Oct 2025 14:30:56 +0300 Subject: [PATCH 13/15] Remove main activity --- app/build.gradle.kts | 4 +- .../accounts/AccountSelectionDialog.kt | 73 -------- .../accounts/AccountSelectionRequest.kt | 47 ++++++ .../anod/appwatcher/sync/SyncNotification.kt | 3 +- .../anod/appwatcher/watchlist/MainActivity.kt | 156 ------------------ .../anod/appwatcher/watchlist/MainScreen.kt | 45 ++++- .../appwatcher/watchlist/MainViewModel.kt | 28 +++- 7 files changed, 111 insertions(+), 245 deletions(-) delete mode 100644 app/src/main/java/com/anod/appwatcher/accounts/AccountSelectionDialog.kt create mode 100644 app/src/main/java/com/anod/appwatcher/accounts/AccountSelectionRequest.kt delete mode 100644 app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01cdcef6..7a9e3848 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,8 +27,8 @@ android { applicationId = "com.anod.appwatcher" minSdk = 31 targetSdk = 36 - versionCode = 16900 - versionName = "1.6.9" + versionCode = 17000 + versionName = "1.7.0" } buildFeatures { 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/sync/SyncNotification.kt b/app/src/main/java/com/anod/appwatcher/sync/SyncNotification.kt index 84b2173f..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,15 +6,14 @@ 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 -import androidx.core.net.toUri /** * @author alex 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 17750915..00000000 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainActivity.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.anod.appwatcher.watchlist - -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.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.anod.appwatcher.AppWatcherActivity -import com.anod.appwatcher.R -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.database.entities.Tag -import com.anod.appwatcher.utils.prefs -import info.anodsplace.applog.AppLog -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 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, - initialTag = Tag.empty - ) - }) - private lateinit var accountSelectionDialog: AccountSelectionDialog - private lateinit var notificationPermissionRequest: ActivityResultLauncher - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - 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.OnBackPressed) - } - } - }) - - 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) - } - } - } - } - } - } - - 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(AppWatcherActivity.searchIntent(this, "", focus = true)) - 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 -> { } - MainViewAction.RequestNotificationPermission -> notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) - MainViewAction.ChooseAccount -> accountSelectionDialog.show() - is MainViewAction.DrawerState -> { } - is MainViewAction.ShowToast -> showToast(action) - is MainViewAction.StartActivity -> startActivity(action) - } - } - -} \ 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 b1384b0f..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,6 +1,10 @@ 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 @@ -14,9 +18,13 @@ 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 @@ -30,6 +38,9 @@ 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) { @@ -49,6 +60,16 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, naviga 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) { @@ -58,7 +79,13 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, naviga drawerState.close() } } else { - onMainAction(action, context, navigateTo) + onMainAction( + action = action, + context = context, + accountSelectionRequest = accountSelectionRequest, + notificationPermissionRequest = notificationPermissionRequest, + navigateTo = navigateTo + ) } } } @@ -89,9 +116,19 @@ fun MainScreenScene(prefs: Preferences, wideLayout: FoldableDeviceLayout, naviga onListEvent = listViewModel::handleEvent, installedApps = listViewModel.installedApps ) + + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + mainViewModel.handleEvent(MainViewEvent.OnResume) + } } -private fun onMainAction(action: MainViewAction, context: Context, navigateTo: (NavKey) -> Unit) { +private fun onMainAction( + action: MainViewAction, + context: Context, + accountSelectionRequest: ManagedActivityResultLauncher, + notificationPermissionRequest: ActivityResultLauncher, + navigateTo: (NavKey) -> Unit +) { when (action) { is MainViewAction.NavigateTo -> { when (action.id) { @@ -104,8 +141,8 @@ private fun onMainAction(action: MainViewAction, context: Context, navigateTo: ( } } is MainViewAction.NavigateToTag -> navigateTo(SceneNavKey.TagWatchList(tag = action.tag)) - MainViewAction.RequestNotificationPermission -> {} //notificationPermissionRequest.launch(AppPermission.PostNotification.toRequestInput()) - MainViewAction.ChooseAccount -> {} //accountSelectionDialog.show() + 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) 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 33395ea4..b104ff54 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/MainViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/MainViewModel.kt @@ -54,14 +54,14 @@ 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 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 object ChooseAccount : 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 @@ -119,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) } @@ -141,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)) @@ -151,6 +157,12 @@ class MainViewModel : BaseFlowViewModel Date: Sat, 25 Oct 2025 14:35:09 +0300 Subject: [PATCH 14/15] Update app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/anod/appwatcher/wishlist/WishListViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 e049e877..b57fb1af 100644 --- a/app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/wishlist/WishListViewModel.kt @@ -161,16 +161,16 @@ class WishListViewModel(wideLayout: FoldableDeviceLayout) : BaseFlowViewModel { - viewState.copy(isError = true) + viewState = viewState.copy(isError = true) if (event.error is CheckTokenError.RequiresInteraction) { emitAction(StartActivity(event.error.intent)) } } WishListEvent.NoAccount -> { - viewState.copy(isError = true) + viewState = viewState.copy(isError = true) } - WishListEvent.RetryClick -> viewState.copy(isError = false) + WishListEvent.RetryClick -> viewState = viewState.copy(isError = false) } } From 7456606dbeb33802becb294f64ca274cef2dc4f2 Mon Sep 17 00:00:00 2001 From: Alexandr Gavrishev Date: Sat, 25 Oct 2025 14:38:18 +0300 Subject: [PATCH 15/15] remove unnecessary parameter --- .../main/java/com/anod/appwatcher/backup/gdrive/GDriveSignIn.kt | 2 +- .../java/com/anod/appwatcher/preferences/SettingsViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 3f9f0ab4..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 @@ -90,7 +90,7 @@ class GDriveSignIn(private val context: ApplicationContext) { }) } - suspend fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = suspendCoroutine { continuation -> + suspend fun onActivityResult(resultCode: Int, data: Intent?) = suspendCoroutine { continuation -> // Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...); if (resultCode == Activity.RESULT_OK && data?.extras != null) { // The Task returned from this call is always completed, no need to attach 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 154de825..9ef08545 100644 --- a/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt +++ b/app/src/main/java/com/anod/appwatcher/preferences/SettingsViewModel.kt @@ -245,7 +245,7 @@ class SettingsViewModel : BaseFlowViewModel