diff --git a/README.md b/README.md index 7fca10cf..24951dc1 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,96 @@

- +

-# Android App Updater +

Android App Updater

-[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Easy%20App%20Updater-brightgreen.svg?style=flat)](https://android-arsenal.com/details/1/7388)[![Codacy Badge](https://app.codacy.com/project/badge/Grade/7e8f094fd77044b5b26bc6c157bfbbc3)](https://app.codacy.com/gh/SirLordPouya/AndroidAppUpdater/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)[![](https://jitpack.io/v/SirLordPouya/AndroidAppUpdater.svg)](https://jitpack.io/#SirLordPouya/AndroidAppUpdater)[![API](https://img.shields.io/badge/API-16%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=16)[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/) +

+ A flexible, lightweight Android library to prompt users to update your app β€” via 16 app stores or direct APK download. +

-**Android App Updater** is a powerful and flexible library to display an update dialog in your Android app. It supports multiple app stores and direct APK download links. Use it easily with **DialogFragment** or **Jetpack Compose**. +

+ JitPack + Android Arsenal + API 23+ + Codacy Badge + License +

- +

-## πŸš€ Installation +--- + +## Features -### Step 1: Add JitPack Repository +- Works with **Jetpack Compose** and **XML Views (DialogFragment)** +- Supports **16 app stores** out of the box (Google Play, Huawei, Samsung, Amazon, and more) +- **Direct APK download** with built-in download management and installation +- **Light, Dark, and System Default** themes +- **Force update** mode (non-dismissable dialog) +- **Custom typeface** support +- **DSL builders** for clean, expressive configuration +- Built-in store icons for all supported stores +- Handles APK installation across all API levels (M through latest) +- Customizable string resources for localization -Add the following to your project’s **build.gradle.kts**: +--- + +## Installation + +### Step 1 β€” Add the JitPack repository + +In your **settings.gradle.kts**: ```kotlin -allprojects { +dependencyResolutionManagement { repositories { maven("https://jitpack.io") } } ``` -Or to your project’s **settings.gradle.kts**: +### Step 2 β€” Add the dependency + +Pick the module that matches your UI toolkit: + ```kotlin -dependencyResolutionManagement { - repositories { - maven ( url = "https://jitpack.io" ) - } +dependencies { + // Jetpack Compose + implementation("com.github.HeyPouya.AndroidAppUpdater:compose:latest_version") + + // XML Views / DialogFragment + implementation("com.github.HeyPouya.AndroidAppUpdater:main:latest_version") } ``` -### Step 2: Add Dependencies +> Replace `latest_version` with the latest release tag from [JitPack](https://jitpack.io/#HeyPouya/AndroidAppUpdater). -```kotlin -// For DialogFragment integration -implementation("com.github.SirLordPouya.AndroidAppUpdater:main:latest_version") - -// For Jetpack Compose integration -implementation("com.github.SirLordPouya.AndroidAppUpdater:compose:latest_version") -``` - -## 🎯 Supported App Stores - -| Store | Enum | -|----------------------|-----------------------------------| -| Google Play | AppStoreType.GOOGLE_PLAY | -| Huawei App Gallery | AppStoreType.HUAWEI_APP_GALLERY | -| Samsung Galaxy Store | AppStoreType.SAMSUNG_GALAXY_STORE | -| Amazon App Store | AppStoreType.AMAZON_APP_STORE | -| Xiaomi GetApp Market | AppStoreType.MI_GET_APP_STORE | -| Oppo App Market | AppStoreType.OPPO_APP_MARKET | -| F-Droid | AppStoreType.FDROID | -| Aptoide | AppStoreType.APTOIDE | -| OneStore | AppStoreType.ONE_STORE_APP_MARKET | -| Vivo V-AppStore | AppStoreType.V_APP_STORE | -| 9-Apps Market | AppStoreType.NINE_APPS_STORE | -| ZTE App Center | AppStoreType.ZTE_APP_CENTER | -| Lenovo App Center | AppStoreType.LENOVO_APP_CENTER | -| Tencent App Store | AppStoreType.TENCENT_APPS_STORE | -| Cafe Bazaar | AppStoreType.CAFE_BAZAAR | -| Myket | AppStoreType.MYKET | - -## πŸ“Œ Usage - -### Define App Stores +--- + +## Quick Start + +### 1. Define your store list ```kotlin -val storesList = listOf( +val stores = listOf( StoreListItem( - store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "YOUR_APP_PACKAGE"), + store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "com.your.package"), title = "Google Play", icon = R.drawable.appupdater_ic_google_play + ), + StoreListItem( + store = StoreFactory.getStore(AppStoreType.HUAWEI_APP_GALLERY, "com.your.package"), + title = "Huawei AppGallery", + icon = R.drawable.appupdater_ic_app_gallery ) ) ``` -### Default Store Icons - -All supported store icons are available in the library. You can use them directly: - -| Market Name | Icon Resource | -|----------------------|--------------------------------------------| -| Google Play | R.drawable.appupdater_ic_google_play | -| Huawei App Gallery | R.drawable.appupdater_ic_app_gallery | -| Samsung Galaxy Store | R.drawable.appupdater_ic_galaxy_store | -| Amazon App Store | R.drawable.appupdater_ic_amazon_app_store | -| Xiaomi GetApp Store | R.drawable.appupdater_ic_get_app_store | -| Oppo App Market | R.drawable.appupdater_ic_oppo_app_market | -| F-Droid App Store | R.drawable.appupdater_ic_fdroid | -| Aptoide App Store | R.drawable.appupdater_ic_aptoide | -| OneStore App Market | R.drawable.appupdater_ic_one_store | -| Vivo V-AppStore | R.drawable.appupdater_ic_v_app_store | -| 9-Apps Market | R.drawable.appupdater_ic_nine_apps | -| ZTE App Center | R.drawable.appupdater_ic_zte_app_center | -| Lenovo App Center | R.drawable.appupdater_ic_lenovo_app_center | -| Tencent App Store | R.drawable.appupdater_ic_tencent_app_store | -| Cafe Bazaar Store | R.drawable.appupdater_ic_bazar | -| Myket App Store | R.drawable.appupdater_ic_myket | - -πŸ‘‰ **Note**: Make sure to import: -```kotlin -import com.pouyaheydari.appupdater.R.* -``` - -### Add a Direct Download Link +### 2. (Optional) Define direct download links -Add the required permissions to your `AndroidManifest.xml`: +Add these permissions to your `AndroidManifest.xml`: ```xml @@ -118,77 +98,282 @@ Add the required permissions to your `AndroidManifest.xml`: ``` -Then create the download link: +Then create the list: ```kotlin -val directDownloadLinksList = listOf( +val directLinks = listOf( DirectDownloadListItem( title = "Direct Download", - url = "https://example.com/app.apk" + url = "https://example.com/your-app.apk" ) ) ``` -### Show the Update Dialog +### 3. Show the dialog -#### βœ… With Jetpack Compose +#### Jetpack Compose ```kotlin -var shouldShowDialog by remember { mutableStateOf(true) } +var showDialog by remember { mutableStateOf(true) } -if (shouldShowDialog) { +if (showDialog) { AndroidAppUpdater( dialogData = UpdaterDialogData( - dialogTitle = "New Update Available", - dialogDescription = "We've fixed bugs and improved performance!", - dividerText = "Or", - storeList = storesList, - directDownloadList = directDownloadLinksList, - onDismissRequested = { shouldShowDialog = false }, - errorWhileOpeningStoreCallback = { storeName -> /* Handle error */ }, - theme = Theme.LIGHT + dialogTitle = "New Update Available!", + dialogDescription = "We've added new features and fixed bugs.", + dividerText = "or", + storeList = stores, + directDownloadList = directLinks, + onDismissRequested = { showDialog = false }, + theme = Theme.SYSTEM_DEFAULT ) ) } ``` -#### βœ… With Fragments +#### DialogFragment (XML Views) ```kotlin -val data = UpdaterDialogData( - title = "New Update Available", - description = "We've fixed bugs and improved performance!", - storeList = storesList, - directDownloadList = directDownloadLinksList, - isForceUpdate = false, - errorWhileOpeningStoreCallback = { storeName -> /* Handle error */ }, - theme = Theme.SYSTEM_DEFAULT, -) +AppUpdaterDialog.getInstance( + UpdaterDialogData( + title = "New Update Available!", + description = "We've added new features and fixed bugs.", + storeList = stores, + directDownloadList = directLinks, + isForceUpdate = false, + theme = Theme.LIGHT + ) +).show(supportFragmentManager, "updater") +``` + +--- + +## DSL Builders + +Both Compose and Fragment APIs offer Kotlin DSL builders for a more expressive syntax. -AppUpdaterDialog.getInstance(data).show(supportFragmentManager, "UPDATE_DIALOG") +#### Fragment DSL + +```kotlin +updateDialogBuilder { + title = "New Update Available!" + description = "We've added new features and fixed bugs." + isForceUpdate = false + theme = Theme.DARK + storeList = listOf( + store { + store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "com.your.package") + title = "Google Play" + icon = R.drawable.appupdater_ic_google_play + } + ) + directDownloadList = listOf( + directDownload { + title = "Direct Download" + url = "https://example.com/your-app.apk" + } + ) + typeface = Typeface.createFromAsset(assets, "fonts/custom.ttf") + errorWhileOpeningStoreCallback = { storeName -> + Toast.makeText(this@MainActivity, "$storeName is not installed", Toast.LENGTH_SHORT).show() + } +}.show(supportFragmentManager, "updater") ``` -## 🎨 Customization +#### Compose DSL -You can override default texts in `strings.xml`: +```kotlin +val dialogData = updaterDialogData { + dialogTitle = "New Update Available!" + dialogDescription = "We've added new features and fixed bugs." + dividerText = "or" + theme = Theme.SYSTEM_DEFAULT + storeList = listOf( + store { + store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "com.your.package") + title = "Google Play" + icon = R.drawable.appupdater_ic_google_play + } + ) + directDownloadList = listOf( + directDownload { + title = "Direct Download" + url = "https://example.com/your-app.apk" + } + ) + onDismissRequested = { /* handle dismiss */ } + errorWhileOpeningStoreCallback = { storeName -> /* handle error */ } +} + +AndroidAppUpdater(dialogData) +``` + +--- + +## Configuration Reference + +### Fragment `UpdaterDialogData` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `title` | `String` | `""` | Title shown at the top of the dialog | +| `description` | `String` | `""` | Description text below the title | +| `storeList` | `List` | `[]` | App stores to show as update options | +| `directDownloadList` | `List` | `[]` | Direct APK download links | +| `isForceUpdate` | `Boolean` | `false` | If `true`, the dialog cannot be dismissed | +| `typeface` | `Typeface?` | `null` | Custom typeface for dialog text | +| `theme` | `Theme` | `SYSTEM_DEFAULT` | `LIGHT`, `DARK`, or `SYSTEM_DEFAULT` | +| `errorWhileOpeningStoreCallback` | `((String) -> Unit)?` | `null` | Called with store name if opening fails | + +### Compose `UpdaterDialogData` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `dialogTitle` | `String` | `""` | Title shown at the top of the dialog | +| `dialogDescription` | `String` | `""` | Description text below the title | +| `dividerText` | `String` | `""` | Text on the divider between stores and downloads | +| `storeList` | `List` | `[]` | App stores to show as update options | +| `directDownloadList` | `List` | `[]` | Direct APK download links | +| `onDismissRequested` | `() -> Unit` | `{}` | Called when the user dismisses the dialog | +| `typeface` | `Typeface?` | `null` | Custom typeface for dialog text | +| `theme` | `Theme` | `SYSTEM_DEFAULT` | `LIGHT`, `DARK`, or `SYSTEM_DEFAULT` | +| `errorWhileOpeningStoreCallback` | `(String) -> Unit` | `{}` | Called with store name if opening fails | + +--- + +## Supported Stores + +| Store | Enum Value | Built-in Icon | +|-------|-----------|---------------| +| Google Play | `GOOGLE_PLAY` | `appupdater_ic_google_play` | +| Cafe Bazaar | `CAFE_BAZAAR` | `appupdater_ic_bazar` | +| Myket | `MYKET` | `appupdater_ic_myket` | +| Huawei AppGallery | `HUAWEI_APP_GALLERY` | `appupdater_ic_app_gallery` | +| Samsung Galaxy Store | `SAMSUNG_GALAXY_STORE` | `appupdater_ic_galaxy_store` | +| Amazon App Store | `AMAZON_APP_STORE` | `appupdater_ic_amazon_app_store` | +| Aptoide | `APTOIDE` | `appupdater_ic_aptoide` | +| F-Droid | `FDROID` | `appupdater_ic_fdroid` | +| Xiaomi GetApps | `MI_GET_APP_STORE` | `appupdater_ic_get_app_store` | +| OneStore | `ONE_STORE_APP_MARKET` | `appupdater_ic_one_store` | +| Oppo App Market | `OPPO_APP_MARKET` | `appupdater_ic_oppo_app_market` | +| Vivo V-AppStore | `V_APP_STORE` | `appupdater_ic_v_app_store` | +| 9Apps | `NINE_APPS_STORE` | `appupdater_ic_nine_apps` | +| Tencent App Store | `TENCENT_APPS_STORE` | `appupdater_ic_tencent_app_store` | +| ZTE App Center | `ZTE_APP_CENTER` | `appupdater_ic_zte_app_center` | +| Lenovo App Center | `LENOVO_APP_CENTER` | `appupdater_ic_lenovo_app_center` | + +All icons are bundled with the library. Use them via `R.drawable.appupdater_ic_*`. + +--- + +## Customization + +### Themes + +Pass one of the `Theme` enum values to control the dialog appearance: + +```kotlin +Theme.LIGHT // Light background, dark text +Theme.DARK // Dark background, light text +Theme.SYSTEM_DEFAULT // Follows the device's current theme +``` + +### Custom Typeface + +```kotlin +val typeface = Typeface.createFromAsset(assets, "fonts/your_font.ttf") + +// Pass it to either API: +UpdaterDialogData( + // ... + typeface = typeface +) +``` + +### String Resources + +Override these in your `strings.xml` to localize or customize dialog text: ```xml - - Please wait - Downloading new version... - Downloading... - Downloading new version - Please install - or - Download from store - +Please wait +Downloading new version... +Downloading... +Downloading new version +Please install +or +Download from store +Couldn't find downloaded file +``` + +--- + +## Architecture + +The library is split into focused modules: + +``` +AndroidAppUpdater/ +β”œβ”€β”€ core/ # Theme enum and shared constants (pure Kotlin) +β”œβ”€β”€ store/ # Store implementations, StoreFactory, icons +β”œβ”€β”€ directdownload/ # APK download via DownloadManager + installation +β”œβ”€β”€ appupdater/ # Fragment/XML UI (published as "main") +β”œβ”€β”€ compose/ # Jetpack Compose UI +└── app/ # Sample/demo application ``` -## πŸ“ License +| Module | Artifact | Description | +|--------|----------|-------------| +| `:core` | `core` | Theme enum and constants β€” no Android dependency | +| `:store` | `store` | All 16 store implementations with built-in icons | +| `:directdownload` | `directdownload` | Download manager, permissions, APK installation | +| `:appupdater` | `main` | DialogFragment-based update dialog | +| `:compose` | `compose` | Jetpack Compose update dialog | + +--- + +## Requirements + +| Requirement | Value | +|------------|-------| +| Min SDK | 23 (Android 6.0) | +| Compile SDK | 36 | +| Kotlin | 2.3+ | +| Java | 17 | + +--- + +## Sample App + +The `:app` module contains a fully working demo with examples for: + +- **Kotlin API** β€” Direct constructor usage +- **DSL API** β€” Builder-style configuration +- **Compose** β€” Composable dialog integration + +Clone the repo and run the `app` module to see the library in action. + +--- + +## License ``` -Android App Updater is released under the Apache License 2.0. See LICENSE for details. -Copyright (c) 2018 Pouya Heydari +Copyright 2018 Pouya Heydari + +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 + + http://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. ``` -

Library icon and design by Amir Gerdakane

+--- + +

+ Library icon and design by Amir Gerdakane +

diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6b4bd23..90afbcac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,5 @@ plugins { alias(libs.plugins.androidApplication) - alias(libs.plugins.jetbrainsKotlinAndroid) alias(libs.plugins.compose.compiler) } @@ -24,10 +23,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlin { - jvmToolchain(17) - } - buildFeatures { compose = true } @@ -38,6 +33,10 @@ android { } } +kotlin { + jvmToolchain(17) +} + dependencies { // library dependency diff --git a/app/src/main/java/com/pouyaheydari/appupdater/demo/utils/Constants.kt b/app/src/main/java/com/pouyaheydari/appupdater/demo/utils/Constants.kt index 4f91520d..b77a1308 100644 --- a/app/src/main/java/com/pouyaheydari/appupdater/demo/utils/Constants.kt +++ b/app/src/main/java/com/pouyaheydari/appupdater/demo/utils/Constants.kt @@ -1,6 +1,6 @@ package com.pouyaheydari.appupdater.demo.utils -internal const val APK_URL = "https://cafebazaar.ir/download/bazaar.apk" +internal const val APK_URL = "https://d.apkpure.com/custom/com.apkpure.aegon-latest.apk" internal const val SAMPLE_PACKAGE_NAME = "com.tencent.mm" internal const val FDROID_SAMPLE_PACKAGE_NAME = "de.storchp.fdroidbuildstatus" internal const val GET_APP_SAMPLE_PACKAGE_NAME = "com.opera.browser" diff --git a/appupdater/consumer-rules.pro b/appupdater/consumer-rules.pro new file mode 100644 index 00000000..22319f17 --- /dev/null +++ b/appupdater/consumer-rules.pro @@ -0,0 +1,17 @@ +# AndroidAppUpdater - AppUpdater (XML/View) module consumer ProGuard rules +# These rules are bundled with the library and applied to consumer projects. + +# Keep the public DialogFragment entry point +-keep class com.pouyaheydari.appupdater.main.ui.AppUpdaterDialog { *; } + +# Keep public API model classes +-keep class com.pouyaheydari.appupdater.main.ui.model.UpdaterDialogData { *; } +-keep class com.pouyaheydari.appupdater.main.ui.model.UpdaterFragmentModel { *; } + +# Keep DSL builder functions and classes +-keep class com.pouyaheydari.appupdater.main.dsl.** { *; } + +# Keep Parcelable CREATOR fields +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/dsl/DSLUtils.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/dsl/DSLUtils.kt index 4fa52c2d..96dd7e91 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/dsl/DSLUtils.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/dsl/DSLUtils.kt @@ -1,22 +1,106 @@ package com.pouyaheydari.appupdater.main.dsl +import android.graphics.Typeface +import com.pouyaheydari.appupdater.core.model.Theme import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem import com.pouyaheydari.appupdater.main.ui.AppUpdaterDialog import com.pouyaheydari.appupdater.main.ui.model.UpdaterDialogData import com.pouyaheydari.appupdater.store.domain.StoreListItem +import com.pouyaheydari.appupdater.store.domain.stores.AppStore +import com.pouyaheydari.appupdater.store.R as storeR /** - * This inline function helps building stores in DSL way + * Mutable builder for [StoreListItem] used in DSL context. */ -inline fun store(block: StoreListItem.() -> Unit): StoreListItem = StoreListItem().apply(block) +class StoreListItemBuilder { + var store: AppStore? = null + var title: String = "" + var icon: Int = storeR.drawable.appupdater_ic_cloud + + fun build(): StoreListItem { + val store = requireNotNull(store) { "StoreListItemBuilder requires 'store' to be set." } + return StoreListItem(store = store, title = title, icon = icon) + } +} + +/** + * Mutable builder for [DirectDownloadListItem] used in DSL context. + */ +class DirectDownloadListItemBuilder { + var title: String = "" + var url: String = "" + + fun build(): DirectDownloadListItem { + require(url.isNotBlank()) { "DirectDownloadListItemBuilder requires 'url' to be set." } + return DirectDownloadListItem(title = title, url = url) + } +} + +/** + * Mutable builder for [UpdaterDialogData] used in DSL context. + */ +class UpdaterDialogDataBuilder { + var title: String = "" + var description: String = "" + var storeList: List = listOf() + var directDownloadList: List = listOf() + var isForceUpdate: Boolean = false + var typeface: Typeface? = null + var errorWhileOpeningStoreCallback: ((String) -> Unit)? = null + var theme: Theme = Theme.SYSTEM_DEFAULT + + fun build(): UpdaterDialogData = UpdaterDialogData( + title = title, + description = description, + storeList = storeList, + directDownloadList = directDownloadList, + isForceUpdate = isForceUpdate, + typeface = typeface, + errorWhileOpeningStoreCallback = errorWhileOpeningStoreCallback, + theme = theme, + ) +} + +/** + * DSL builder for constructing a [StoreListItem]. + * + * Example usage: + * ``` + * val item = store { + * store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "com.example.app") + * title = "Google Play" + * icon = R.drawable.ic_google_play + * } + * ``` + */ +inline fun store(block: StoreListItemBuilder.() -> Unit): StoreListItem = + StoreListItemBuilder().apply(block).build() /** - * This inline function helps building direct download links in DSL way + * DSL builder for constructing a [DirectDownloadListItem]. + * + * Example usage: + * ``` + * val item = directDownload { + * title = "Direct APK" + * url = "https://example.com/app.apk" + * } + * ``` */ -inline fun directDownload(block: DirectDownloadListItem.() -> Unit): DirectDownloadListItem = DirectDownloadListItem().apply(block) +inline fun directDownload(block: DirectDownloadListItemBuilder.() -> Unit): DirectDownloadListItem = + DirectDownloadListItemBuilder().apply(block).build() /** - * This inline function helps building UpdateDialog in DSL way + * DSL builder for constructing and obtaining an [AppUpdaterDialog] instance. + * + * Example usage: + * ``` + * val dialog = updateDialogBuilder { + * title = "New Update Available" + * description = "Version 2.0 is ready" + * storeList = listOf(...) + * } + * ``` */ -inline fun updateDialogBuilder(block: UpdaterDialogData.() -> Unit): AppUpdaterDialog = - AppUpdaterDialog.getInstance(UpdaterDialogData().apply(block)) +inline fun updateDialogBuilder(block: UpdaterDialogDataBuilder.() -> Unit): AppUpdaterDialog = + AppUpdaterDialog.getInstance(UpdaterDialogDataBuilder().apply(block).build()) diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterDialog.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterDialog.kt index 24e11d80..1eefee5f 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterDialog.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterDialog.kt @@ -18,7 +18,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem -import com.pouyaheydari.appupdater.directdownload.utils.donwloadapk.checkPermissionsAndDownloadApk +import com.pouyaheydari.appupdater.directdownload.utils.downloadapk.checkPermissionsAndDownloadApk import com.pouyaheydari.appupdater.directdownload.utils.installapk.installAPK import com.pouyaheydari.appupdater.main.R import com.pouyaheydari.appupdater.main.data.mapper.mapToSelectedTheme @@ -62,7 +62,9 @@ class AppUpdaterDialog : DialogFragment() { // Getting data passed to the library val data = arguments?.parcelable(UPDATE_DIALOG_KEY) ?: UpdaterFragmentModel.EMPTY if (data == UpdaterFragmentModel.EMPTY || (data.storeList.isEmpty() && data.directDownloadList.isEmpty())) { - throw IllegalArgumentException("Invalid data provided to the updater dialog. Either 'storeList' or 'directDownloadList' must be non-empty. For more details, refer to the documentation at $UPDATE_DIALOG_README_URL") + throw IllegalArgumentException( + "Invalid data provided to the updater dialog. Either 'storeList' or 'directDownloadList' must be non-empty. For more details, refer to the documentation at $UPDATE_DIALOG_README_URL", + ) } setDialogBackground(mapToSelectedTheme(data.theme, requireContext())) isCancelable = data.isForceUpdate @@ -95,7 +97,19 @@ class AppUpdaterDialog : DialogFragment() { } private fun subscribeToViewModel(theme: UserSelectedTheme) { - viewModel.screenState.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) + // Observe persistent UI state (survives config changes) + viewModel.screenState + .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) + .onEach { uiState -> + when (uiState) { + DialogScreenUiState.Idle -> hideUpdateInProgressDialog() + DialogScreenUiState.UpdateInProgress -> showUpdateInProgressDialog(theme) + } + }.launchIn(lifecycleScope) + + // Observe one-shot side-effects (consumed exactly once, no replay on config change) + viewModel.sideEffect + .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) .onEach { when (it) { is DialogScreenStates.DownloadApk -> { @@ -105,7 +119,7 @@ class AppUpdaterDialog : DialogFragment() { androidSdkVersion = Build.VERSION.SDK_INT, notificationTitle = requireContext().getString(com.pouyaheydari.appupdater.directdownload.R.string.appupdater_download_notification_title), notificationDescription = requireContext().getString(com.pouyaheydari.appupdater.directdownload.R.string.appupdater_download_notification_desc), - downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager, ) { viewModel.handleIntent(DialogScreenIntents.OnApkDownloadStarted) } @@ -121,9 +135,6 @@ class AppUpdaterDialog : DialogFragment() { viewModel.handleIntent(DialogScreenIntents.OnErrorCallbackExecuted) } - DialogScreenStates.HideUpdateInProgress -> hideUpdateInProgressDialog() - DialogScreenStates.ShowUpdateInProgress -> showUpdateInProgressDialog(theme) - DialogScreenStates.Empty -> hideUpdateInProgressDialog() is DialogScreenStates.InstallApk -> installDownloadedApk(it) } }.launchIn(lifecycleScope) @@ -265,7 +276,7 @@ class AppUpdaterDialog : DialogFragment() { storeList, directDownloadList, !isForceUpdate, - theme + theme, ) TypefaceHolder.typeface = typeface diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModel.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModel.kt index 307192cb..ec2483df 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModel.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModel.kt @@ -9,32 +9,55 @@ import com.pouyaheydari.appupdater.main.ui.model.DialogScreenIntents import com.pouyaheydari.appupdater.main.ui.model.DialogScreenStates import com.pouyaheydari.appupdater.main.utils.ErrorCallbackHolder import com.pouyaheydari.appupdater.main.utils.TypefaceHolder +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +/** + * ViewModel for the app updater dialog (XML/View module). + * + * Uses [StateFlow] for persistent UI state (e.g. update-in-progress indicator) and + * [Channel] for one-shot side-effects (e.g. open store, download APK) that should + * not replay on configuration change. + */ internal class AppUpdaterViewModel( private val isUpdateInProgressUseCase: GetDownloadStateUseCase, - private val setDownloadStateUseCase: SetDownloadStateUseCase + private val setDownloadStateUseCase: SetDownloadStateUseCase, ) : ViewModel() { - val screenState = MutableStateFlow(DialogScreenStates.HideUpdateInProgress) + /** Persistent UI state that survives configuration changes. */ + private val _screenState = MutableStateFlow(DialogScreenUiState.Idle) + val screenState: StateFlow = _screenState.asStateFlow() + + /** One-shot side-effect events consumed exactly once by the UI. */ + private val _sideEffect = Channel(Channel.BUFFERED) + val sideEffect = _sideEffect.receiveAsFlow() fun handleIntent(intent: DialogScreenIntents) { when (intent) { - is DialogScreenIntents.OnDirectLinkClicked -> screenState.value = DialogScreenStates.DownloadApk(intent.item.url) - is DialogScreenIntents.OnStoreClicked -> screenState.value = DialogScreenStates.OpenStore(intent.item.store) - DialogScreenIntents.OnStoreOpened -> screenState.value = DialogScreenStates.Empty - DialogScreenIntents.OnErrorCallbackExecuted -> screenState.value = DialogScreenStates.Empty - DialogScreenIntents.OnApkDownloadRequested -> screenState.value = DialogScreenStates.Empty + is DialogScreenIntents.OnDirectLinkClicked -> + _sideEffect.trySend(DialogScreenStates.DownloadApk(intent.item.url)) + + is DialogScreenIntents.OnStoreClicked -> + _sideEffect.trySend(DialogScreenStates.OpenStore(intent.item.store)) + + is DialogScreenIntents.OnOpeningStoreFailed -> + _sideEffect.trySend(DialogScreenStates.ExecuteErrorCallback(intent.store.getUserReadableName())) + DialogScreenIntents.OnApkDownloadStarted -> { setUpdateInProgress() observeUpdateInProgressStatus() } - is DialogScreenIntents.OnOpeningStoreFailed -> - screenState.value = DialogScreenStates.ExecuteErrorCallback(intent.store.getUserReadableName()) - - DialogScreenIntents.OnApkInstallationStarted -> screenState.value = DialogScreenStates.Empty + // These intents signal the UI consumed a side-effect; no further action needed. + DialogScreenIntents.OnStoreOpened, + DialogScreenIntents.OnErrorCallbackExecuted, + DialogScreenIntents.OnApkDownloadRequested, + DialogScreenIntents.OnApkInstallationStarted, + -> { /* consumed */ } } } @@ -47,12 +70,17 @@ internal class AppUpdaterViewModel( private fun observeUpdateInProgressStatus() { viewModelScope.launch { isUpdateInProgressUseCase().collectLatest { downloadState -> - screenState.value = when (downloadState) { - is DownloadState.Downloaded -> - DialogScreenStates.InstallApk(downloadState.apk) + when (downloadState) { + is DownloadState.Downloaded -> { + _screenState.value = DialogScreenUiState.Idle + _sideEffect.trySend(DialogScreenStates.InstallApk(downloadState.apk)) + } is DownloadState.Downloading -> - DialogScreenStates.ShowUpdateInProgress + _screenState.value = DialogScreenUiState.UpdateInProgress + + is DownloadState.Failed -> + _screenState.value = DialogScreenUiState.Idle } } } @@ -64,3 +92,15 @@ internal class AppUpdaterViewModel( super.onCleared() } } + +/** + * Persistent UI state for the updater dialog. + * This state survives configuration changes and is safe to replay. + */ +internal sealed interface DialogScreenUiState { + /** No download in progress; idle state. */ + data object Idle : DialogScreenUiState + + /** An APK download is in progress; show the progress indicator. */ + data object UpdateInProgress : DialogScreenUiState +} diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/DirectRecyclerAdapter.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/DirectRecyclerAdapter.kt index 09a1f9be..d10d336e 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/DirectRecyclerAdapter.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/DirectRecyclerAdapter.kt @@ -11,16 +11,19 @@ internal class DirectRecyclerAdapter( private val list: List, private val typeface: Typeface?, private val listener: (DirectDownloadListItem) -> Unit, -) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SoresViewHolder = - DownloadDirectItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - .run { SoresViewHolder(this) } +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DirectDownloadViewHolder = + DownloadDirectItemBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + .run { DirectDownloadViewHolder(this) } override fun getItemCount(): Int = list.size - override fun onBindViewHolder(holder: SoresViewHolder, position: Int) = holder.onBind(list[position]) + override fun onBindViewHolder(holder: DirectDownloadViewHolder, position: Int) = holder.onBind(list[position]) - inner class SoresViewHolder(private val binding: DownloadDirectItemBinding) : RecyclerView.ViewHolder(binding.root) { + inner class DirectDownloadViewHolder( + private val binding: DownloadDirectItemBinding, + ) : RecyclerView.ViewHolder(binding.root) { fun onBind(item: DirectDownloadListItem) { val txtDirect = binding.txtDirect txtDirect.text = item.title diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/StoresRecyclerAdapter.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/StoresRecyclerAdapter.kt index 04fc3f98..2122587e 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/StoresRecyclerAdapter.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/StoresRecyclerAdapter.kt @@ -17,16 +17,19 @@ internal class StoresRecyclerAdapter( private val theme: UserSelectedTheme, private val typeface: Typeface?, private val listener: (StoreListItem) -> Unit, -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - DownloadStoresItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - .run { SoresViewHolder(this) } + DownloadStoresItemBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + .run { StoresViewHolder(this) } override fun getItemCount(): Int = list.size - override fun onBindViewHolder(holder: SoresViewHolder, position: Int) = holder.onBind(list[position]) + override fun onBindViewHolder(holder: StoresViewHolder, position: Int) = holder.onBind(list[position]) - inner class SoresViewHolder(private val binding: DownloadStoresItemBinding) : RecyclerView.ViewHolder(binding.root) { + inner class StoresViewHolder( + private val binding: DownloadStoresItemBinding, + ) : RecyclerView.ViewHolder(binding.root) { fun onBind(item: StoreListItem) { val txtStoreTitle = binding.txtStoreTitle val imgStore = binding.imgStore diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/DialogScreenStates.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/DialogScreenStates.kt index 9d203d01..4ce92ab9 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/DialogScreenStates.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/DialogScreenStates.kt @@ -4,9 +4,6 @@ import com.pouyaheydari.appupdater.store.domain.stores.AppStore import java.io.File internal sealed interface DialogScreenStates { - data object Empty : DialogScreenStates - data object ShowUpdateInProgress : DialogScreenStates - data object HideUpdateInProgress : DialogScreenStates data class OpenStore(val store: AppStore) : DialogScreenStates data class DownloadApk(val apkUrl: String) : DialogScreenStates data class ExecuteErrorCallback(val storeName: String) : DialogScreenStates diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterDialogData.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterDialogData.kt index 07dacea0..2ceae311 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterDialogData.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterDialogData.kt @@ -6,15 +6,24 @@ import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem import com.pouyaheydari.appupdater.store.domain.StoreListItem /** - * This model is used to pass the data to dialog fragment via bundles + * Configuration data for the updater dialog. + * + * @property title the title displayed at the top of the dialog + * @property description the description text displayed below the title + * @property storeList list of app stores to display as update options + * @property directDownloadList list of direct APK download links + * @property isForceUpdate when true, the dialog cannot be dismissed by the user + * @property typeface optional custom typeface for all text in the dialog + * @property errorWhileOpeningStoreCallback optional callback invoked with the store name when opening a store fails + * @property theme the visual theme for the dialog */ data class UpdaterDialogData( - var title: String = "", - var description: String = "", - var storeList: List = listOf(), - var directDownloadList: List = listOf(), - var isForceUpdate: Boolean = false, - var typeface: Typeface? = null, - var errorWhileOpeningStoreCallback: ((String) -> Unit)? = null, - var theme: Theme = Theme.SYSTEM_DEFAULT, + val title: String = "", + val description: String = "", + val storeList: List = listOf(), + val directDownloadList: List = listOf(), + val isForceUpdate: Boolean = false, + val typeface: Typeface? = null, + val errorWhileOpeningStoreCallback: ((String) -> Unit)? = null, + val theme: Theme = Theme.SYSTEM_DEFAULT, ) diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterFragmentModel.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterFragmentModel.kt index 3ccbeab7..00b72c05 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterFragmentModel.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterFragmentModel.kt @@ -1,51 +1,24 @@ package com.pouyaheydari.appupdater.main.ui.model -import android.os.Parcel import android.os.Parcelable import com.pouyaheydari.appupdater.core.model.Theme import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem import com.pouyaheydari.appupdater.store.domain.StoreListItem +import kotlinx.parcelize.Parcelize /** - * This model is used to pass the data to dialog fragment via bundles + * This model is used to pass the data to dialog fragment via bundles. */ +@Parcelize internal data class UpdaterFragmentModel( - var title: String = "", - var description: String = "", - var storeList: List = listOf(), - var directDownloadList: List = listOf(), - var isForceUpdate: Boolean = false, - var theme: Theme = Theme.SYSTEM_DEFAULT, + val title: String = "", + val description: String = "", + val storeList: List = listOf(), + val directDownloadList: List = listOf(), + val isForceUpdate: Boolean = false, + val theme: Theme = Theme.SYSTEM_DEFAULT, ) : Parcelable { - constructor(parcel: Parcel) : this( - parcel.readString().orEmpty(), - parcel.readString().orEmpty(), - parcel.createTypedArrayList(StoreListItem).orEmpty(), - parcel.createTypedArrayList(DirectDownloadListItem).orEmpty(), - parcel.readByte() != 0.toByte(), - Theme.entries[parcel.readInt()], - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(title) - parcel.writeString(description) - parcel.writeTypedList(storeList) - parcel.writeByte(if (isForceUpdate) 1 else 0) - parcel.writeInt(theme.ordinal) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { + companion object { val EMPTY = UpdaterFragmentModel() - override fun createFromParcel(parcel: Parcel): UpdaterFragmentModel { - return UpdaterFragmentModel(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } } } diff --git a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/ErrorCallbackHolder.kt b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/ErrorCallbackHolder.kt index 1e3197d9..daac3ec9 100644 --- a/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/ErrorCallbackHolder.kt +++ b/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/ErrorCallbackHolder.kt @@ -1,9 +1,7 @@ package com.pouyaheydari.appupdater.main.utils -import android.graphics.Typeface - /** - * Holds an instance of [Typeface] to be used in dialogs while being shown. + * Holds an error callback lambda to be invoked when opening a store fails. */ internal object ErrorCallbackHolder { var callback: ((String) -> Unit)? = null diff --git a/appupdater/src/test/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModelTest.kt b/appupdater/src/test/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModelTest.kt index 4433042a..9c4ce8dc 100644 --- a/appupdater/src/test/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModelTest.kt +++ b/appupdater/src/test/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModelTest.kt @@ -24,7 +24,6 @@ import org.mockito.kotlin.whenever import java.io.File class AppUpdaterViewModelTest { - private lateinit var viewModel: AppUpdaterViewModel private val isUpdateInProgressUseCase: GetDownloadStateUseCase = mock() private val setDownloadStateUseCase: SetDownloadStateUseCase = mock() @@ -38,24 +37,22 @@ class AppUpdaterViewModelTest { } @Test - fun `handleIntent should update screen state for OnDirectLinkClicked`() = runTest { + fun `handleIntent should emit side effect for OnDirectLinkClicked`() = runTest { val testUrl = "https://example.com/app.apk" val testItem = DirectDownloadListItem(title = "Test", url = testUrl) - viewModel.screenState.test { + viewModel.sideEffect.test { viewModel.handleIntent(DialogScreenIntents.OnDirectLinkClicked(testItem)) - assertEquals(DialogScreenStates.HideUpdateInProgress, awaitItem()) assertEquals(DialogScreenStates.DownloadApk(testUrl), awaitItem()) } } @Test - fun `handleIntent should update screen state for OnStoreClicked`() = runTest { + fun `handleIntent should emit side effect for OnStoreClicked`() = runTest { val testItem = StoreListItem(store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "package"), title = "Google Play", icon = 0) - viewModel.screenState.test { + viewModel.sideEffect.test { viewModel.handleIntent(DialogScreenIntents.OnStoreClicked(testItem)) - assertEquals(DialogScreenStates.HideUpdateInProgress, awaitItem()) assertEquals(DialogScreenStates.OpenStore(testItem.store), awaitItem()) } } @@ -65,10 +62,9 @@ class AppUpdaterViewModelTest { val file: File = mock() whenever(isUpdateInProgressUseCase()).thenReturn(flowOf(DownloadState.Downloading, DownloadState.Downloaded(file))) - viewModel.screenState.test { + viewModel.sideEffect.test { viewModel.handleIntent(DialogScreenIntents.OnApkDownloadStarted) verify(setDownloadStateUseCase).invoke(DownloadState.Downloading) - assertEquals(DialogScreenStates.HideUpdateInProgress, awaitItem()) assertEquals(DialogScreenStates.InstallApk(file), awaitItem()) } } diff --git a/build-logic/convention/src/main/java/com/pouyaheydari/appupdater/convention/plugins/AndroidLibraryPlugin.kt b/build-logic/convention/src/main/java/com/pouyaheydari/appupdater/convention/plugins/AndroidLibraryPlugin.kt index 78019ace..fb44c749 100644 --- a/build-logic/convention/src/main/java/com/pouyaheydari/appupdater/convention/plugins/AndroidLibraryPlugin.kt +++ b/build-logic/convention/src/main/java/com/pouyaheydari/appupdater/convention/plugins/AndroidLibraryPlugin.kt @@ -1,22 +1,20 @@ package com.pouyaheydari.appupdater.convention.plugins -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import com.pouyaheydari.appupdater.convention.helpers.baseLibs import com.pouyaheydari.appupdater.convention.helpers.compileSdk import com.pouyaheydari.appupdater.convention.helpers.javaVersion import com.pouyaheydari.appupdater.convention.helpers.minSdk import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.kotlin.dsl.configure -import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension class AndroidLibraryPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { apply(baseLibs.findPlugin("androidLibrary").get().get().pluginId) - apply(baseLibs.findPlugin("jetbrainsKotlinAndroid").get().get().pluginId) + apply(baseLibs.findPlugin("kotlin.parcelize").get().get().pluginId) apply(baseLibs.findPlugin("maven.publish").get().get().pluginId) } @@ -25,17 +23,13 @@ class AndroidLibraryPlugin : Plugin { defaultConfig { minSdk = minSdk() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") } compileOptions { sourceCompatibility = javaVersion() targetCompatibility = javaVersion() } } - extensions.configure { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } - } } } } diff --git a/build.gradle.kts b/build.gradle.kts index 2a601f8b..2307af0d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ plugins { alias(libs.plugins.androidApplication) apply false - alias(libs.plugins.jetbrainsKotlinAndroid) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.maven.publish) apply false diff --git a/compose/consumer-rules.pro b/compose/consumer-rules.pro new file mode 100644 index 00000000..03b78404 --- /dev/null +++ b/compose/consumer-rules.pro @@ -0,0 +1,13 @@ +# AndroidAppUpdater - Compose module consumer ProGuard rules +# These rules are bundled with the library and applied to consumer projects. + +# Keep the public composable entry point (referenced by name in consumer code) +-keep class com.pouyaheydari.appupdater.compose.ui.AndroidAppUpdaterScreenKt { *; } + +# Keep public API model classes +-keep class com.pouyaheydari.appupdater.compose.ui.models.UpdaterDialogData { *; } + +# Keep Parcelable CREATOR fields +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} diff --git a/compose/src/main/java/com/pouyaheydari/appupdater/compose/dsl/DSLUtils.kt b/compose/src/main/java/com/pouyaheydari/appupdater/compose/dsl/DSLUtils.kt new file mode 100644 index 00000000..e8078467 --- /dev/null +++ b/compose/src/main/java/com/pouyaheydari/appupdater/compose/dsl/DSLUtils.kt @@ -0,0 +1,113 @@ +package com.pouyaheydari.appupdater.compose.dsl + +import android.graphics.Typeface +import com.pouyaheydari.appupdater.compose.ui.models.UpdaterDialogData +import com.pouyaheydari.appupdater.core.model.Theme +import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem +import com.pouyaheydari.appupdater.store.domain.StoreListItem +import com.pouyaheydari.appupdater.store.domain.stores.AppStore +import com.pouyaheydari.appupdater.store.R as storeR + +/** + * Mutable builder for [StoreListItem] used in DSL context. + */ +class StoreListItemBuilder { + var store: AppStore? = null + var title: String = "" + var icon: Int = storeR.drawable.appupdater_ic_cloud + + fun build(): StoreListItem { + val store = requireNotNull(store) { "StoreListItemBuilder requires 'store' to be set." } + return StoreListItem(store = store, title = title, icon = icon) + } +} + +/** + * Mutable builder for [DirectDownloadListItem] used in DSL context. + */ +class DirectDownloadListItemBuilder { + var title: String = "" + var url: String = "" + + fun build(): DirectDownloadListItem { + require(url.isNotBlank()) { "DirectDownloadListItemBuilder requires 'url' to be set." } + return DirectDownloadListItem(title = title, url = url) + } +} + +/** + * Mutable builder for [UpdaterDialogData] used in DSL context. + */ +class UpdaterDialogDataBuilder { + var dialogTitle: String = "" + var dialogDescription: String = "" + var dividerText: String = "" + var storeList: List = listOf() + var directDownloadList: List = listOf() + var onDismissRequested: () -> Unit = {} + var errorWhileOpeningStoreCallback: (String) -> Unit = {} + var typeface: Typeface? = null + var theme: Theme = Theme.SYSTEM_DEFAULT + + fun build(): UpdaterDialogData = UpdaterDialogData( + dialogTitle = dialogTitle, + dialogDescription = dialogDescription, + dividerText = dividerText, + storeList = storeList, + directDownloadList = directDownloadList, + onDismissRequested = onDismissRequested, + errorWhileOpeningStoreCallback = errorWhileOpeningStoreCallback, + typeface = typeface, + theme = theme, + ) +} + +/** + * DSL builder for constructing a [StoreListItem]. + * + * Example usage: + * ``` + * val item = store { + * store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "com.example.app") + * title = "Google Play" + * icon = R.drawable.ic_google_play + * } + * ``` + */ +inline fun store(block: StoreListItemBuilder.() -> Unit): StoreListItem = + StoreListItemBuilder().apply(block).build() + +/** + * DSL builder for constructing a [DirectDownloadListItem]. + * + * Example usage: + * ``` + * val item = directDownload { + * title = "Direct APK" + * url = "https://example.com/app.apk" + * } + * ``` + */ +inline fun directDownload(block: DirectDownloadListItemBuilder.() -> Unit): DirectDownloadListItem = + DirectDownloadListItemBuilder().apply(block).build() + +/** + * DSL builder for constructing an [UpdaterDialogData] for the Compose updater. + * + * Example usage: + * ``` + * val dialogData = updaterDialogData { + * dialogTitle = "New Update Available" + * dialogDescription = "Version 2.0 is ready" + * storeList = listOf( + * store { + * store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "com.example.app") + * title = "Google Play" + * } + * ) + * theme = Theme.SYSTEM_DEFAULT + * } + * ``` + */ +inline fun updaterDialogData(block: UpdaterDialogDataBuilder.() -> Unit): UpdaterDialogData = + UpdaterDialogDataBuilder().apply(block).build() diff --git a/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterScreen.kt b/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterScreen.kt index 6fd6c0b0..5fb32915 100644 --- a/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterScreen.kt +++ b/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterScreen.kt @@ -26,7 +26,7 @@ import com.pouyaheydari.appupdater.compose.ui.utils.previewDirectDownloadListDat import com.pouyaheydari.appupdater.compose.ui.utils.previewStoreListData import com.pouyaheydari.appupdater.core.model.Theme import com.pouyaheydari.appupdater.core.utils.ANDROID_APP_UPDATER_DEBUG_TAG -import com.pouyaheydari.appupdater.directdownload.utils.donwloadapk.checkPermissionsAndDownloadApk +import com.pouyaheydari.appupdater.directdownload.utils.downloadapk.checkPermissionsAndDownloadApk import com.pouyaheydari.appupdater.directdownload.utils.installapk.installAPK import com.pouyaheydari.appupdater.store.domain.AppStoreCallback import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore @@ -35,9 +35,10 @@ import java.io.File import com.pouyaheydari.appupdater.directdownload.R as directDownloadR /** - * Use this composable to show the updater dialog. + * Entry-point composable for showing the Android App Updater dialog. * - * @param dialogData is th + * @param dialogData configuration for the updater dialog including title, description, + * store list, direct download links, theme, and callbacks */ @Composable fun AndroidAppUpdater(dialogData: UpdaterDialogData) { @@ -143,7 +144,7 @@ private fun setupDirectApkDownload( url = url, notificationTitle = notificationTitle, notificationDescription = notificationDescription, - onDownloadingApkStarted = onDownloadingApkStarted + onDownloadingApkStarted = onDownloadingApkStarted, ) onDownloadApkRequested() } @@ -154,7 +155,7 @@ private fun getApkIfActivityIsNotNull( url: String, notificationTitle: String, notificationDescription: String, - onDownloadingApkStarted: () -> Unit + onDownloadingApkStarted: () -> Unit, ) { if (activity == null) { Log.e(ANDROID_APP_UPDATER_DEBUG_TAG, "Provided activity is null. Skipping downloading the apk") @@ -166,7 +167,7 @@ private fun getApkIfActivityIsNotNull( notificationTitle = notificationTitle, notificationDescription = notificationDescription, downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager, - onDownloadingApkStarted = onDownloadingApkStarted + onDownloadingApkStarted = onDownloadingApkStarted, ) } } diff --git a/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterViewModel.kt b/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterViewModel.kt index e87c62b7..ae531f5b 100644 --- a/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterViewModel.kt +++ b/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterViewModel.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.launch internal class AndroidAppUpdaterViewModel( viewModelData: UpdaterViewModelData, private val getDownloadStateUseCase: GetDownloadStateUseCase, - private val setDownloadStateUseCase: SetDownloadStateUseCase + private val setDownloadStateUseCase: SetDownloadStateUseCase, ) : ViewModel() { private val _uiState = MutableStateFlow(DialogScreenState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -90,6 +90,9 @@ internal class AndroidAppUpdaterViewModel( is DownloadState.Downloading -> _uiState.update { it.copy(downloadState = it.downloadState.copy(shouldShowUpdateInProgress = true)) } + + is DownloadState.Failed -> + _uiState.update { it.copy(downloadState = it.downloadState.copy(shouldShowUpdateInProgress = false)) } } } } diff --git a/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/UpdaterDialogData.kt b/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/UpdaterDialogData.kt index df9bbdca..f7b97f0d 100644 --- a/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/UpdaterDialogData.kt +++ b/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/UpdaterDialogData.kt @@ -6,7 +6,17 @@ import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem import com.pouyaheydari.appupdater.store.domain.StoreListItem /** - * This model is used to pass data to config the [com.pouyaheydari.appupdater.compose.ui.AndroidAppUpdater] + * Configuration data for the [com.pouyaheydari.appupdater.compose.ui.AndroidAppUpdater] composable. + * + * @property dialogTitle title text shown at the top of the update dialog + * @property dialogDescription description text shown below the title + * @property dividerText text displayed on the divider between store list and direct download list + * @property storeList list of app stores the user can choose to update from + * @property directDownloadList list of direct APK download links + * @property onDismissRequested callback invoked when the user requests to dismiss the dialog + * @property errorWhileOpeningStoreCallback callback invoked with the store name when a store fails to open + * @property typeface optional custom [Typeface] applied to all text in the dialog + * @property theme the visual [Theme] for the dialog (light, dark, or system default) */ data class UpdaterDialogData( val dialogTitle: String = "", diff --git a/directdownload/consumer-rules.pro b/directdownload/consumer-rules.pro new file mode 100644 index 00000000..882ddd26 --- /dev/null +++ b/directdownload/consumer-rules.pro @@ -0,0 +1,18 @@ +# AndroidAppUpdater - DirectDownload module consumer ProGuard rules +# These rules are bundled with the library and applied to consumer projects. + +# Keep public API data classes +-keep class com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem { *; } +-keep class com.pouyaheydari.appupdater.directdownload.domain.DownloadState { *; } +-keep class com.pouyaheydari.appupdater.directdownload.domain.DownloadState$* { *; } + +# Keep Parcelable CREATOR fields +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} + +# Keep the BroadcastReceiver declared in the manifest +-keep class com.pouyaheydari.appupdater.directdownload.receiver.DownloadFinishedReceiver { *; } + +# Keep the FileProvider subclass referenced in the manifest +-keep class com.pouyaheydari.appupdater.directdownload.utils.downloadapk.APKFileProviderImpl { *; } diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/data/DirectDownloadListItem.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/data/DirectDownloadListItem.kt index c2b12e95..3db25c90 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/data/DirectDownloadListItem.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/data/DirectDownloadListItem.kt @@ -1,28 +1,16 @@ package com.pouyaheydari.appupdater.directdownload.data -import android.os.Parcel import android.os.Parcelable +import kotlinx.parcelize.Parcelize -data class DirectDownloadListItem(var title: String = "", var url: String = "") : Parcelable { - private constructor(parcel: Parcel) : this( - parcel.readString().orEmpty(), - parcel.readString().orEmpty(), - ) - - override fun describeContents(): Int = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeString(title) - dest.writeString(url) - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): DirectDownloadListItem { - return DirectDownloadListItem(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} +/** + * Represents a direct download item to be displayed in the updater dialog. + * + * @property title the display title shown to the user + * @property url the direct URL to the APK file + */ +@Parcelize +data class DirectDownloadListItem( + val title: String = "", + val url: String = "", +) : Parcelable diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/DownloadState.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/DownloadState.kt index 33b4b77b..603ff7ab 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/DownloadState.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/DownloadState.kt @@ -2,7 +2,24 @@ package com.pouyaheydari.appupdater.directdownload.domain import java.io.File +/** + * Represents the current state of an APK download. + */ sealed interface DownloadState { + /** The APK is currently being downloaded. */ data object Downloading : DownloadState - data class Downloaded(val apk: File) : DownloadState + + /** The APK has been downloaded successfully. */ + data class Downloaded( + val apk: File, + ) : DownloadState + + /** + * The APK download failed. + * + * @property reason a human-readable description of the failure, or null if unknown + */ + data class Failed( + val reason: String? = null, + ) : DownloadState } diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/receiver/DownloadFinishedReceiver.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/receiver/DownloadFinishedReceiver.kt index abf54c9e..f9f20342 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/receiver/DownloadFinishedReceiver.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/receiver/DownloadFinishedReceiver.kt @@ -8,7 +8,7 @@ import com.pouyaheydari.appupdater.directdownload.data.UpdateInProgressRepositor import com.pouyaheydari.appupdater.directdownload.domain.DownloadState import com.pouyaheydari.appupdater.directdownload.domain.GetRequestIdUseCase import com.pouyaheydari.appupdater.directdownload.domain.SetDownloadStateUseCase -import com.pouyaheydari.appupdater.directdownload.utils.donwloadapk.APKFileProviderImpl +import com.pouyaheydari.appupdater.directdownload.utils.downloadapk.APKFileProviderImpl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKDownloadManager.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKDownloadManager.kt similarity index 93% rename from directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKDownloadManager.kt rename to directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKDownloadManager.kt index c80576ec..336f6ff3 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKDownloadManager.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKDownloadManager.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.app.DownloadManager import android.content.Context @@ -35,7 +35,7 @@ class APKDownloadManager( uri = url.toUri(), context = context, notificationTitle = notificationTitle, - notificationDescription = notificationDescription + notificationDescription = notificationDescription, ) setRequestIdUseCase(downloadManager.enqueue(downloadManagerRequest)) } diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProvider.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProvider.kt similarity index 66% rename from directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProvider.kt rename to directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProvider.kt index 9105b95e..8299f670 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProvider.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProvider.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.content.Context import java.io.File diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProviderImpl.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProviderImpl.kt similarity index 83% rename from directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProviderImpl.kt rename to directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProviderImpl.kt index 7e9a8b76..e77dcfba 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProviderImpl.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProviderImpl.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.content.Context import android.os.Environment diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadAPKHelper.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadAPKHelper.kt similarity index 86% rename from directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadAPKHelper.kt rename to directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadAPKHelper.kt index 0b781e11..bf31938c 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadAPKHelper.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadAPKHelper.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.app.Activity import android.app.DownloadManager @@ -14,7 +14,7 @@ fun checkPermissionsAndDownloadApk( downloadManager: DownloadManager, downloadAPKPermission: DownloadAPKPermission = DownloadAPKPermissionFactory().getDownloadAPKPermissionHandler(androidSdkVersion), apkDownloadManager: APKDownloadManager = APKDownloadManager(), - onDownloadingApkStarted: () -> Unit + onDownloadingApkStarted: () -> Unit, ) { if (downloadAPKPermission.resolvePermissions(activity)) { apkDownloadManager.deleteExistingAPKAndDownloadNewAPK( @@ -22,7 +22,7 @@ fun checkPermissionsAndDownloadApk( context = activity, notificationTitle = notificationTitle, notificationDescription = notificationDescription, - downloadManager = downloadManager + downloadManager = downloadManager, ) onDownloadingApkStarted() } diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadManagerRequestCreator.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadManagerRequestCreator.kt similarity index 93% rename from directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadManagerRequestCreator.kt rename to directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadManagerRequestCreator.kt index ae4893e2..a6fb6f34 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadManagerRequestCreator.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadManagerRequestCreator.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.app.DownloadManager import android.app.DownloadManager.Request.NETWORK_MOBILE @@ -12,13 +12,12 @@ import com.pouyaheydari.appupdater.core.utils.ANDROID_APP_UPDATER_DEBUG_TAG import com.pouyaheydari.appupdater.core.utils.APK_NAME class DownloadManagerRequestCreator { - fun create( uri: Uri, context: Context, notificationTitle: String, notificationDescription: String, - downloadManagerRequest: DownloadManager.Request = DownloadManager.Request(uri) + downloadManagerRequest: DownloadManager.Request = DownloadManager.Request(uri), ): DownloadManager.Request = downloadManagerRequest.apply { setTitle(notificationTitle) setDescription(notificationDescription) diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactory.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactory.kt index 86061885..e448cea2 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactory.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactory.kt @@ -1,9 +1,8 @@ package com.pouyaheydari.appupdater.directdownload.utils.permission class DownloadAPKPermissionFactory { - fun getDownloadAPKPermissionHandler(androidSdkVersion: Int): DownloadAPKPermission = when { androidSdkVersion >= android.os.Build.VERSION_CODES.P -> DownloadAPKPermissionForPAndAbove() - else -> DownloadAPKPermissionForOAndBellow() + else -> DownloadAPKPermissionForOAndBelow() } } diff --git a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBellow.kt b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBelow.kt similarity index 78% rename from directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBellow.kt rename to directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBelow.kt index dc4d8218..01bbe35e 100644 --- a/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBellow.kt +++ b/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBelow.kt @@ -10,16 +10,13 @@ import androidx.core.content.ContextCompat private const val PERMISSION_REQUEST_CODE = 2000 -internal class DownloadAPKPermissionForOAndBellow : DownloadAPKPermission { - +internal class DownloadAPKPermissionForOAndBelow : DownloadAPKPermission { @RequiresApi(Build.VERSION_CODES.O) - override fun resolvePermissions(activity: Activity): Boolean { - return if (isExternalStoragePermissionGranted(activity)) { - true - } else { - getWriteToStoragePermission(activity) - false - } + override fun resolvePermissions(activity: Activity): Boolean = if (isExternalStoragePermissionGranted(activity)) { + true + } else { + getWriteToStoragePermission(activity) + false } private fun isExternalStoragePermissionGranted(activity: Activity) = diff --git a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKDownloadManagerTest.kt b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKDownloadManagerTest.kt similarity index 91% rename from directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKDownloadManagerTest.kt rename to directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKDownloadManagerTest.kt index 7673cc6f..4d0179a2 100644 --- a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKDownloadManagerTest.kt +++ b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKDownloadManagerTest.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.app.DownloadManager import android.content.Context @@ -16,7 +16,6 @@ import java.io.File @RunWith(RobolectricTestRunner::class) class APKDownloadManagerTest { - private val downloadManager: DownloadManager = mock() private val context: Context = mock() private val downloadManagerRequestCreator: DownloadManagerRequestCreator = mock() @@ -31,7 +30,7 @@ class APKDownloadManagerTest { apkDownloadManager = APKDownloadManager( downloadManagerRequestCreator, setRequestIdUseCase, - apkFileProvider + apkFileProvider, ) } @@ -51,7 +50,7 @@ class APKDownloadManagerTest { url, context, notificationTitle, - notificationDescription + notificationDescription, ) verify(apkFileProvider).getFile(context) @@ -72,7 +71,11 @@ class APKDownloadManagerTest { whenever(downloadManager.enqueue(any())).thenReturn(5678L) apkDownloadManager.deleteExistingAPKAndDownloadNewAPK( - downloadManager, url, context, notificationTitle, notificationDescription + downloadManager, + url, + context, + notificationTitle, + notificationDescription, ) verify(apkFileProvider).getFile(context) diff --git a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProviderImplTest.kt b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProviderImplTest.kt similarity index 94% rename from directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProviderImplTest.kt rename to directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProviderImplTest.kt index a9d88fb2..6dbe7e14 100644 --- a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProviderImplTest.kt +++ b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/APKFileProviderImplTest.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.content.Context import android.os.Environment @@ -14,7 +14,6 @@ import java.io.File @RunWith(MockitoJUnitRunner::class) class APKFileProviderImplTest { - private val context: Context = mock() private lateinit var apkFileProvider: APKFileProviderImpl diff --git a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadAPKHelperKtTest.kt b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadAPKHelperKtTest.kt similarity index 93% rename from directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadAPKHelperKtTest.kt rename to directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadAPKHelperKtTest.kt index 99b367bc..ad04551e 100644 --- a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadAPKHelperKtTest.kt +++ b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadAPKHelperKtTest.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.app.Activity import android.app.DownloadManager @@ -12,7 +12,6 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.whenever class DownloadAPKHelperTest { - private val url = "https://example.com/app.apk" private val notificationTitle = "New Update" private val notificationDescription = "Downloading latest version" @@ -36,7 +35,7 @@ class DownloadAPKHelperTest { downloadManager, downloadAPKPermission, apkDownloadManager, - onDownloadingApkStarted + onDownloadingApkStarted, ) verify(apkDownloadManager).deleteExistingAPKAndDownloadNewAPK( @@ -62,7 +61,7 @@ class DownloadAPKHelperTest { downloadManager, downloadAPKPermission, apkDownloadManager, - onDownloadingApkStarted + onDownloadingApkStarted, ) verify(apkDownloadManager, never()).deleteExistingAPKAndDownloadNewAPK( @@ -70,7 +69,7 @@ class DownloadAPKHelperTest { anyString(), anyOrNull(), anyString(), - anyOrNull() + anyOrNull(), ) verify(onDownloadingApkStarted, never()).invoke() } diff --git a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadManagerRequestCreatorTest.kt b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadManagerRequestCreatorTest.kt similarity index 96% rename from directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadManagerRequestCreatorTest.kt rename to directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadManagerRequestCreatorTest.kt index 87f69c45..2ebbaae3 100644 --- a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadManagerRequestCreatorTest.kt +++ b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/downloadapk/DownloadManagerRequestCreatorTest.kt @@ -1,4 +1,4 @@ -package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk +package com.pouyaheydari.appupdater.directdownload.utils.downloadapk import android.app.DownloadManager import android.content.Context @@ -15,7 +15,6 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class DownloadManagerRequestCreatorTest { - private val context: Context = mock() private val downloadManagerRequestCreator: DownloadManagerRequestCreator = DownloadManagerRequestCreator() diff --git a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactoryTest.kt b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactoryTest.kt index bf4eacb3..5f8f07fa 100644 --- a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactoryTest.kt +++ b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactoryTest.kt @@ -5,7 +5,6 @@ import org.junit.Assert.assertTrue import org.junit.Test class DownloadAPKPermissionFactoryTest { - private val factory = DownloadAPKPermissionFactory() @Test @@ -21,7 +20,7 @@ class DownloadAPKPermissionFactoryTest { } @Test - fun `getDownloadAPKPermissionHandler should return DownloadAPKPermissionForOAndBellow for Android O and below`() { + fun `getDownloadAPKPermissionHandler should return DownloadAPKPermissionForOAndBelow for Android O and below`() { // Arrange val androidSdkVersion = Build.VERSION_CODES.O @@ -29,6 +28,6 @@ class DownloadAPKPermissionFactoryTest { val result = factory.getDownloadAPKPermissionHandler(androidSdkVersion) // Assert - assertTrue(result is DownloadAPKPermissionForOAndBellow) + assertTrue(result is DownloadAPKPermissionForOAndBelow) } } diff --git a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBellowTest.kt b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBelowTest.kt similarity index 95% rename from directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBellowTest.kt rename to directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBelowTest.kt index 7caf0983..cd1af2a0 100644 --- a/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBellowTest.kt +++ b/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBelowTest.kt @@ -15,10 +15,9 @@ import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class DownloadAPKPermissionForOAndBellowTest { - +class DownloadAPKPermissionForOAndBelowTest { private val mockActivity: Activity = mock() - private val permissionHandler = DownloadAPKPermissionForOAndBellow() + private val permissionHandler = DownloadAPKPermissionForOAndBelow() @Test fun `resolvePermissions should return true when permission is granted`() { @@ -49,7 +48,7 @@ class DownloadAPKPermissionForOAndBellowTest { ActivityCompat.requestPermissions( mockActivity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), - 2000 + 2000, ) } } diff --git a/gradle.properties b/gradle.properties index 07bbf1ef..eab04f6f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ android.nonFinalResIds=true android.useAndroidX=true org.gradle.parallel=true POM_GROUP_ID=com.pouyaheydari.updater -POM_VERSION=11.0.2 +POM_VERSION=11.1.0 POM_DESCRIPTION=App Updater is an easy-to-use and fully customizable library to show update dialog to users. POM_URL=https://github.com/HeyPouya/AndroidAppUpdater POM_SCM_URL=https://github.com/HeyPouya/AndroidAppUpdater diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 00000000..9fed0a0d --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/ecd23fd7707c683afbcd6052998cb6a9/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/2ddfb13e430f2b3a94c9c937d8d2f67e/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/e7337738591f6120002875ec9a5cf45c/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3454cd3e..a156eb29 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,10 +3,10 @@ appVersion = "1000" compileSdkVersion = "36" minSdkVersion = "23" targetSdkVersion = "36" -agp = "8.13.2" -kotlin = "2.2.21" +agp = "9.1.0" +kotlin = "2.3.20" appcompat = "1.7.1" -androidXCore = "1.17.0" +androidXCore = "1.18.0" constraintLayout = "2.2.1" junit4 = "4.13.2" androidTestJUnit = "1.3.0" @@ -16,14 +16,14 @@ recyclerView = "1.4.0" lifecycle = "2.10.0" coroutines = "1.10.2" fragment = "1.8.9" -androidxComposeBom = "2025.12.00" -composeActivity = "1.12.1" -mockito = "5.21.0" -mockitoKotlin = "6.1.0" -roboelectric = "4.16" +androidxComposeBom = "2026.03.00" +composeActivity = "1.13.0" +mockito = "5.23.0" +mockitoKotlin = "6.3.0" +roboelectric = "4.16.1" turbine = "1.2.1" uiautomator = "2.3.0" -mavenPublish = "0.35.0" +mavenPublish = "0.36.0" [libraries] androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } @@ -60,8 +60,8 @@ kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-pl [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } -jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } project-androidLibrary = { id = "com.pouyaheydari.androidLibraryPlugin"} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 18f4d987..73eb6c49 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.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index f6da1a18..de9aba68 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) diff --git a/store/consumer-rules.pro b/store/consumer-rules.pro new file mode 100644 index 00000000..7bbe867a --- /dev/null +++ b/store/consumer-rules.pro @@ -0,0 +1,10 @@ +# AndroidAppUpdater - Store module consumer ProGuard rules +# These rules are bundled with the library and applied to consumer projects. + +# Keep all public API classes (domain models, interfaces, enums) +-keep class com.pouyaheydari.appupdater.store.domain.** { *; } + +# Keep Parcelable CREATOR fields for all AppStore implementations +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/AppStoreCallback.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/AppStoreCallback.kt index 937c1f0d..ba6ecdec 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/AppStoreCallback.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/AppStoreCallback.kt @@ -1,9 +1,31 @@ package com.pouyaheydari.appupdater.store.domain -import android.content.ActivityNotFoundException import com.pouyaheydari.appupdater.store.domain.stores.AppStore +/** + * Sealed interface representing the result of attempting to open an app store. + * + * @see [showAppInSelectedStore] + */ sealed interface AppStoreCallback { - data class Success(val store: AppStore) : AppStoreCallback - data class Failure(val store: AppStore, val exception: ActivityNotFoundException) : AppStoreCallback + /** + * Indicates the store was opened successfully. + * + * @property store the [AppStore] that was opened + */ + data class Success( + val store: AppStore, + ) : AppStoreCallback + + /** + * Indicates the store could not be opened. + * + * @property store the [AppStore] that failed to open + * @property exception the exception that caused the failure (e.g. [android.content.ActivityNotFoundException] + * when no activity can handle the store intent, or [IllegalStateException] when context is null) + */ + data class Failure( + val store: AppStore, + val exception: Exception, + ) : AppStoreCallback } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreFactory.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreFactory.kt index d33c3a51..013a917b 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreFactory.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreFactory.kt @@ -35,7 +35,21 @@ import com.pouyaheydari.appupdater.store.domain.stores.TencentAppStore import com.pouyaheydari.appupdater.store.domain.stores.VAppStore import com.pouyaheydari.appupdater.store.domain.stores.ZTEAppCenter +/** + * Factory that creates [AppStore] instances from [AppStoreType] and a package name. + * + * Example usage: + * ``` + * val store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "com.example.app") + * ``` + */ object StoreFactory { + /** + * Returns an [AppStore] for the given [storeType] configured with [packageName]. + * + * @param storeType the type of store to create + * @param packageName the application package name to open in that store + */ fun getStore(storeType: AppStoreType, packageName: String): AppStore = when (storeType) { GOOGLE_PLAY -> GooglePlayStore(packageName) CAFE_BAZAAR -> CafeBazaarStore(packageName) diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreIntentBuilder.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreIntentBuilder.kt index deca4b61..b9b1186b 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreIntentBuilder.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreIntentBuilder.kt @@ -3,16 +3,33 @@ package com.pouyaheydari.appupdater.store.domain import android.content.Intent import android.net.Uri +/** + * Internal builder for creating store-opening [Intent]s. + */ internal object StoreIntentBuilder { - internal class Builder(private val uriString: String) { + /** + * Builder for constructing an [Intent] to view a URI, optionally scoped to a specific store package. + * + * @param uriString the URI string to open (e.g. a market:// or https:// URL) + */ + internal class Builder( + private val uriString: String, + ) { private var storePackageName: String? = null + /** + * Restricts the intent to the given store package. + * + * @param storePackageName the package name of the store app + * @throws IllegalArgumentException if [storePackageName] is blank + */ fun withPackage(storePackageName: String): Builder { - require(storePackageName.isNotBlank()) { "Store's package name most not be empty" } + require(storePackageName.isNotBlank()) { "Store's package name must not be empty" } this.storePackageName = storePackageName return this } + /** Builds and returns the configured [Intent]. */ fun build(): Intent = Intent(Intent.ACTION_VIEW, Uri.parse(uriString)).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK storePackageName?.takeIf { it.isNotBlank() }?.let { setPackage(it) } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreListItem.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreListItem.kt index 7f77b3f9..4c578d6b 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreListItem.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreListItem.kt @@ -1,38 +1,20 @@ package com.pouyaheydari.appupdater.store.domain -import android.os.Parcel import android.os.Parcelable -import androidx.core.os.ParcelCompat import com.pouyaheydari.appupdater.store.R import com.pouyaheydari.appupdater.store.domain.stores.AppStore -import com.pouyaheydari.appupdater.store.domain.stores.AppStoreType +import kotlinx.parcelize.Parcelize +/** + * Represents a store item to be displayed in the updater dialog. + * + * @property store the [AppStore] implementation to open when the user taps this item + * @property title the display title shown to the user + * @property icon the drawable resource ID for the store icon + */ +@Parcelize data class StoreListItem( - var store: AppStore = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, ""), - var title: String = "", - var icon: Int = R.drawable.appupdater_ic_cloud, -) : Parcelable { - private constructor(parcel: Parcel) : this( - ParcelCompat.readParcelable(parcel, AppStore::class.java.classLoader, AppStore::class.java) ?: StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, ""), - parcel.readString().orEmpty(), - parcel.readInt(), - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeParcelable(store, flags) - parcel.writeString(title) - parcel.writeInt(icon) - } - - override fun describeContents(): Int = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): StoreListItem { - return StoreListItem(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} + val store: AppStore, + val title: String = "", + val icon: Int = R.drawable.appupdater_ic_cloud, +) : Parcelable diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreManager.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreManager.kt index 738510bb..5e96cbad 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreManager.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreManager.kt @@ -4,10 +4,21 @@ import android.content.ActivityNotFoundException import android.content.Context import com.pouyaheydari.appupdater.store.domain.stores.AppStore +/** + * Attempts to open the given [store] in the corresponding app store. + * + * @param context the Android context used to start the store activity + * @param store the [AppStore] to open + * @param callback invoked with [AppStoreCallback.Success] on success or [AppStoreCallback.Failure] on failure + */ fun showAppInSelectedStore(context: Context?, store: AppStore, callback: (AppStoreCallback) -> Unit) { + if (context == null) { + callback(AppStoreCallback.Failure(store, IllegalStateException("Context is null, cannot open store"))) + return + } try { val intent = store.getIntent() - context?.startActivity(intent) + context.startActivity(intent) callback(AppStoreCallback.Success(store)) } catch (exception: ActivityNotFoundException) { callback(AppStoreCallback.Failure(store, exception)) diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AmazonAppStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AmazonAppStore.kt index 6b909701..cf45e3fe 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AmazonAppStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AmazonAppStore.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val AMAZON_APP_STORE_URL = "amzn://apps/android?p=" internal const val AMAZON_PACKAGE = "com.amazon.venezia" @@ -10,9 +9,10 @@ internal const val AMAZON_PACKAGE = "com.amazon.venezia" /** * Opens application's page in [Amazon App Store](https://www.amazon.com/gp/mas/get/amazonapp) */ -internal data class AmazonAppStore(val packageName: String) : AppStore { - private constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class AmazonAppStore( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$AMAZON_APP_STORE_URL$packageName") @@ -22,20 +22,4 @@ internal data class AmazonAppStore(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.AMAZON_APP_STORE override fun getUserReadableName(): String = AppStoreType.AMAZON_APP_STORE.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): AmazonAppStore { - return AmazonAppStore(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStore.kt index 542e3abe..3e2b6e88 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStore.kt @@ -3,8 +3,21 @@ package com.pouyaheydari.appupdater.store.domain.stores import android.content.Intent import android.os.Parcelable +/** + * Strategy interface for an app store. + * + * Each implementation represents a specific app store (e.g. Google Play, Cafe Bazaar) + * and knows how to build the [Intent] to open an app's page in that store. + * + * Implementations must be [Parcelable] so they can be passed through Bundle arguments. + */ interface AppStore : Parcelable { + /** Returns the [Intent] that opens this app's page in the store. */ fun getIntent(): Intent + + /** Returns the [AppStoreType] enum value identifying this store. */ fun getType(): AppStoreType + + /** Returns a human-readable display name for this store (e.g. "Google Play"). */ fun getUserReadableName(): String } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStoreType.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStoreType.kt index 4d4d9bb8..ad15e49f 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStoreType.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStoreType.kt @@ -1,6 +1,15 @@ package com.pouyaheydari.appupdater.store.domain.stores -enum class AppStoreType(internal val userReadableName: String) { +/** + * Enum of all supported app stores. + * + * Each entry carries a [userReadableName] that can be displayed in the UI. + * Use [StoreFactory][com.pouyaheydari.appupdater.store.domain.StoreFactory] to obtain + * an [AppStore] instance from a given type. + */ +enum class AppStoreType( + internal val userReadableName: String, +) { GOOGLE_PLAY("Google Play"), CAFE_BAZAAR("Cafe Bazaar"), MYKET("Myket"), diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/Aptoide.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/Aptoide.kt index 221efbbe..555c10aa 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/Aptoide.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/Aptoide.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val APTOIDE_URL = "aptoideinstall://package=" internal const val APTOIDE_PACKAGE = "cm.aptoide.pt" @@ -10,9 +9,10 @@ internal const val APTOIDE_PACKAGE = "cm.aptoide.pt" /** * Opens application's page in [Aptoide App Store](https://en.aptoide.com/) */ -internal data class Aptoide(val packageName: String) : AppStore { - private constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class Aptoide( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$APTOIDE_URL$packageName") .withPackage(APTOIDE_PACKAGE) @@ -21,20 +21,4 @@ internal data class Aptoide(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.APTOIDE override fun getUserReadableName(): String = AppStoreType.APTOIDE.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): Aptoide { - return Aptoide(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/CafeBazaarStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/CafeBazaarStore.kt index 1e667590..33f382a8 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/CafeBazaarStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/CafeBazaarStore.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val BAZAAR_URL = "bazaar://details?id=" internal const val BAZAAR_PACKAGE = "com.farsitel.bazaar" @@ -10,9 +9,10 @@ internal const val BAZAAR_PACKAGE = "com.farsitel.bazaar" /** * Opens application's page in [CafeBazaar App Store](https://cafebazaar.ir) */ -internal data class CafeBazaarStore(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class CafeBazaarStore( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$BAZAAR_URL$packageName") .withPackage(BAZAAR_PACKAGE) @@ -21,20 +21,4 @@ internal data class CafeBazaarStore(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.CAFE_BAZAAR override fun getUserReadableName(): String = AppStoreType.CAFE_BAZAAR.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): CafeBazaarStore { - return CafeBazaarStore(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/FDroid.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/FDroid.kt index 0ac8c99c..342ad330 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/FDroid.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/FDroid.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val FDROID_URL = "fdroid.app://details?id=" internal const val FDROID_PACKAGE = "org.fdroid.fdroid" @@ -10,9 +9,10 @@ internal const val FDROID_PACKAGE = "org.fdroid.fdroid" /** * Opens application's page in [F-Droid App Store](https://f-droid.org/) */ -internal data class FDroid(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class FDroid( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$FDROID_URL$packageName") .withPackage(FDROID_PACKAGE) @@ -21,20 +21,4 @@ internal data class FDroid(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.FDROID override fun getUserReadableName(): String = AppStoreType.FDROID.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): FDroid { - return FDroid(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/GooglePlayStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/GooglePlayStore.kt index 7a83bcae..33205213 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/GooglePlayStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/GooglePlayStore.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val PLAY_URL = "market://details?id=" internal const val PLAY_PACKAGE = "com.android.vending" @@ -10,9 +9,10 @@ internal const val PLAY_PACKAGE = "com.android.vending" /** * Opens application's page in [GooglePlay Store](https://play.google.com) */ -internal data class GooglePlayStore(val packageName: String) : AppStore { - private constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class GooglePlayStore( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$PLAY_URL$packageName") .withPackage(PLAY_PACKAGE) @@ -21,20 +21,4 @@ internal data class GooglePlayStore(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.GOOGLE_PLAY override fun getUserReadableName(): String = AppStoreType.GOOGLE_PLAY.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): GooglePlayStore { - return GooglePlayStore(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/HuaweiAppGallery.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/HuaweiAppGallery.kt index f94256fb..a9a3f3ae 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/HuaweiAppGallery.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/HuaweiAppGallery.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val HUAWEI_APP_GALLERY_URL = "appmarket://details?id=" internal const val HUAWEI_APP_GALLERY_PACKAGE = "com.huawei.appmarket" @@ -10,9 +9,10 @@ internal const val HUAWEI_APP_GALLERY_PACKAGE = "com.huawei.appmarket" /** * Opens application's page in [Huawei App Gallery](https://appgallery.huawei.com/) */ -internal data class HuaweiAppGallery(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class HuaweiAppGallery( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$HUAWEI_APP_GALLERY_URL$packageName") .withPackage(HUAWEI_APP_GALLERY_PACKAGE) @@ -21,20 +21,4 @@ internal data class HuaweiAppGallery(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.HUAWEI_APP_GALLERY override fun getUserReadableName(): String = AppStoreType.HUAWEI_APP_GALLERY.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): HuaweiAppGallery { - return HuaweiAppGallery(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/LenovoAppCenter.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/LenovoAppCenter.kt index 93378076..78c08f36 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/LenovoAppCenter.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/LenovoAppCenter.kt @@ -1,17 +1,17 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val LENOVO_APP_CENTER_URL = "leapp://ptn/appinfo.do?pn=" /** * Opens application's page in [Lenovo App Store](https://www.lenovomm.com/) */ -internal data class LenovoAppCenter(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class LenovoAppCenter( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$LENOVO_APP_CENTER_URL$packageName") .build() @@ -19,20 +19,4 @@ internal data class LenovoAppCenter(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.LENOVO_APP_CENTER override fun getUserReadableName(): String = AppStoreType.LENOVO_APP_CENTER.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): LenovoAppCenter { - return LenovoAppCenter(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MiGetAppStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MiGetAppStore.kt index b4c5d318..d9397f59 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MiGetAppStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MiGetAppStore.kt @@ -1,17 +1,17 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val MI_APP_STORE_URL = "mimarket://details?id=" /** * Opens application's page in [Xiaomi GetApp store](https://global.app.mi.com/) */ -internal data class MiGetAppStore(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class MiGetAppStore( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$MI_APP_STORE_URL$packageName") @@ -20,20 +20,4 @@ internal data class MiGetAppStore(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.MI_GET_APP_STORE override fun getUserReadableName(): String = AppStoreType.MI_GET_APP_STORE.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): MiGetAppStore { - return MiGetAppStore(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MyketStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MyketStore.kt index 873d65dc..10186bb5 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MyketStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MyketStore.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val MYKET_URL = "myket://details?id=" internal const val MYKET_PACKAGE = "ir.mservices.market" @@ -10,9 +9,10 @@ internal const val MYKET_PACKAGE = "ir.mservices.market" /** * Opens application's page in [Myket Store](https://myket.ir/) */ -internal data class MyketStore(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class MyketStore( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$MYKET_URL$packageName") .withPackage(MYKET_PACKAGE) @@ -21,20 +21,4 @@ internal data class MyketStore(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.MYKET override fun getUserReadableName(): String = AppStoreType.MYKET.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): MyketStore { - return MyketStore(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/NineApps.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/NineApps.kt index e8f08e06..a30e3fee 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/NineApps.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/NineApps.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val NINE_APPS_STORE_URL = "nineapps://AppDetail?id=" internal const val NINE_APPS_PACKAGE = "com.gamefun.apk2u" @@ -10,9 +9,10 @@ internal const val NINE_APPS_PACKAGE = "com.gamefun.apk2u" /** * Opens application's page in [9-Apps](https://www.9apps.com/) */ -internal data class NineApps(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class NineApps( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$NINE_APPS_STORE_URL$packageName") .withPackage(NINE_APPS_PACKAGE) @@ -21,20 +21,4 @@ internal data class NineApps(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.NINE_APPS_STORE override fun getUserReadableName(): String = AppStoreType.NINE_APPS_STORE.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): NineApps { - return NineApps(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OneStoreAppMarket.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OneStoreAppMarket.kt index 26b37d23..c52792f8 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OneStoreAppMarket.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OneStoreAppMarket.kt @@ -1,17 +1,17 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val ONE_STORE_APP_MARKET_URL = "onestore://common/product/" /** * Opens application's page in [OneStore App Market](https://m.onestore.co.kr/mobilepoc/main/main.omp) */ -internal data class OneStoreAppMarket(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class OneStoreAppMarket( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$ONE_STORE_APP_MARKET_URL$packageName") @@ -20,20 +20,4 @@ internal data class OneStoreAppMarket(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.ONE_STORE_APP_MARKET override fun getUserReadableName(): String = AppStoreType.ONE_STORE_APP_MARKET.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): OneStoreAppMarket { - return OneStoreAppMarket(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OppoAppMarket.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OppoAppMarket.kt index 898721e9..71dcd5ba 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OppoAppMarket.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OppoAppMarket.kt @@ -1,8 +1,7 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val OPPO_APP_MARKET_URL = "market://details?id=" internal const val OPPO_PACKAGE = "com.heytap.market" @@ -10,9 +9,10 @@ internal const val OPPO_PACKAGE = "com.heytap.market" /** * Opens application's page in [OppoAppMarket](https://oppomobile.com/) */ -internal data class OppoAppMarket(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class OppoAppMarket( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$OPPO_APP_MARKET_URL$packageName") .withPackage(OPPO_PACKAGE) @@ -21,20 +21,4 @@ internal data class OppoAppMarket(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.OPPO_APP_MARKET override fun getUserReadableName(): String = AppStoreType.OPPO_APP_MARKET.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): OppoAppMarket { - return OppoAppMarket(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/SamsungGalaxyStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/SamsungGalaxyStore.kt index ab049558..9054c17f 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/SamsungGalaxyStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/SamsungGalaxyStore.kt @@ -1,17 +1,17 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val SAMSUNG_GALAXY_STORE_URL = "samsungapps://ProductDetail/" /** * Opens application's page in [Samsung Galaxy store](https://www.samsung.com/de/apps/galaxy-store/) */ -internal data class SamsungGalaxyStore(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class SamsungGalaxyStore( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$SAMSUNG_GALAXY_STORE_URL$packageName") .build() @@ -19,20 +19,4 @@ internal data class SamsungGalaxyStore(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.SAMSUNG_GALAXY_STORE override fun getUserReadableName(): String = AppStoreType.SAMSUNG_GALAXY_STORE.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): SamsungGalaxyStore { - return SamsungGalaxyStore(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/TencentAppStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/TencentAppStore.kt index f7768df9..fa3dee6c 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/TencentAppStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/TencentAppStore.kt @@ -1,17 +1,17 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val TENCENT_APP_STORE_URL = "tmast://appdetails?pname=" /** * Opens application's page in [Tencent App Store](https://appstore.tencent.com/) */ -internal data class TencentAppStore(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class TencentAppStore( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$TENCENT_APP_STORE_URL$packageName") .build() @@ -19,20 +19,4 @@ internal data class TencentAppStore(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.TENCENT_APPS_STORE override fun getUserReadableName(): String = AppStoreType.TENCENT_APPS_STORE.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): TencentAppStore { - return TencentAppStore(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/VAppStore.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/VAppStore.kt index eb99d7cd..b8a65a19 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/VAppStore.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/VAppStore.kt @@ -1,17 +1,17 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val V_APP_STORE_URL = "vivoMarket://details?id=" /** * Opens application's page in [V-AppStore](https://developer.vivo.com/home) */ -internal data class VAppStore(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class VAppStore( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$V_APP_STORE_URL$packageName") .build() @@ -19,20 +19,4 @@ internal data class VAppStore(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.V_APP_STORE override fun getUserReadableName(): String = AppStoreType.V_APP_STORE.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): VAppStore { - return VAppStore(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/ZTEAppCenter.kt b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/ZTEAppCenter.kt index 830b3245..0fa29739 100644 --- a/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/ZTEAppCenter.kt +++ b/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/ZTEAppCenter.kt @@ -1,17 +1,17 @@ package com.pouyaheydari.appupdater.store.domain.stores -import android.os.Parcel -import android.os.Parcelable import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder +import kotlinx.parcelize.Parcelize internal const val ZTE_APP_CENTER_URL = "zte_market://appdetails?pname=" /** * Opens application's page in [ZTE App Store](https://apps.ztems.com/) */ -internal data class ZTEAppCenter(val packageName: String) : AppStore { - constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) - +@Parcelize +internal data class ZTEAppCenter( + val packageName: String, +) : AppStore { override fun getIntent() = StoreIntentBuilder .Builder("$ZTE_APP_CENTER_URL$packageName") .build() @@ -19,20 +19,4 @@ internal data class ZTEAppCenter(val packageName: String) : AppStore { override fun getType(): AppStoreType = AppStoreType.ZTE_APP_CENTER override fun getUserReadableName(): String = AppStoreType.ZTE_APP_CENTER.userReadableName - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(packageName) - } - - override fun describeContents() = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ZTEAppCenter { - return ZTEAppCenter(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } }