├── .editorconfig
├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── assets
│ └── fonts
│ │ └── vazir.ttf
│ ├── java
│ └── com
│ │ └── pouyaheydari
│ │ └── appupdater
│ │ └── demo
│ │ ├── ui
│ │ ├── android
│ │ │ └── MainActivity.kt
│ │ └── compose
│ │ │ ├── ComposeSampleActivity.kt
│ │ │ └── theme
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ └── utils
│ │ ├── ComposePreviewData.kt
│ │ ├── Constants.kt
│ │ └── DSLPreviewData.kt
│ └── res
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── appupdater
├── .gitignore
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── pouyaheydari
│ │ │ └── appupdater
│ │ │ └── main
│ │ │ ├── data
│ │ │ └── mapper
│ │ │ │ └── SelectedThemeMapper.kt
│ │ │ ├── dsl
│ │ │ └── DSLUtils.kt
│ │ │ ├── ui
│ │ │ ├── AppUpdaterDialog.kt
│ │ │ ├── AppUpdaterViewModel.kt
│ │ │ ├── AppUpdaterViewModelFactory.kt
│ │ │ ├── UpdateInProgressDialog.kt
│ │ │ ├── adapters
│ │ │ │ ├── DirectRecyclerAdapter.kt
│ │ │ │ └── StoresRecyclerAdapter.kt
│ │ │ └── model
│ │ │ │ ├── DialogScreenIntents.kt
│ │ │ │ ├── DialogScreenStates.kt
│ │ │ │ ├── UpdaterDialogData.kt
│ │ │ │ ├── UpdaterFragmentModel.kt
│ │ │ │ └── UserSelectedTheme.kt
│ │ │ └── utils
│ │ │ ├── EnumBundleExtension.kt
│ │ │ ├── ErrorCallbackHolder.kt
│ │ │ ├── GetDialogWidth.kt
│ │ │ ├── ParcelableCompatApi.kt
│ │ │ └── TypefaceHolder.kt
│ └── res
│ │ ├── drawable
│ │ ├── dialog_background.xml
│ │ ├── dialog_background_dark.xml
│ │ └── update_in_progress_dialog_background_dark.xml
│ │ ├── layout
│ │ ├── download_direct_item.xml
│ │ ├── download_stores_item.xml
│ │ ├── fragment_app_updater_dialog.xml
│ │ └── fragment_update_in_progress_dialog.xml
│ │ └── values
│ │ └── colors.xml
│ └── test
│ └── java
│ └── com
│ └── pouyaheydari
│ └── appupdater
│ └── main
│ ├── data
│ └── mapper
│ │ └── SelectedThemeMapperTest.kt
│ └── ui
│ └── AppUpdaterViewModelTest.kt
├── build.gradle.kts
├── compose
├── .gitignore
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── pouyaheydari
│ │ └── appupdater
│ │ └── compose
│ │ └── ui
│ │ └── components
│ │ └── AppUpdaterDialogComponentAndroidTest.kt
│ ├── debug
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── pouyaheydari
│ │ └── appupdater
│ │ └── compose
│ │ └── ComposeTestActivity.kt
│ ├── main
│ └── java
│ │ └── com
│ │ └── pouyaheydari
│ │ └── appupdater
│ │ └── compose
│ │ ├── data
│ │ └── mapper
│ │ │ ├── UpdaterDialogUIMapper.kt
│ │ │ └── UpdaterViewModelDataMapper.kt
│ │ └── ui
│ │ ├── AndroidAppUpdaterScreen.kt
│ │ ├── AndroidAppUpdaterViewModel.kt
│ │ ├── AndroidAppUpdaterViewModelFactory.kt
│ │ ├── components
│ │ ├── AppUpdaterDialogComponent.kt
│ │ ├── DialogHeaderComponent.kt
│ │ ├── DirectDownloadLinkComponent.kt
│ │ ├── DividerComponent.kt
│ │ ├── SquareStoreItemComponent.kt
│ │ └── UpdateInProgressDialogComponent.kt
│ │ ├── models
│ │ ├── DialogHeaderModel.kt
│ │ ├── DialogScreenIntents.kt
│ │ ├── DialogScreenState.kt
│ │ ├── UpdaterDialogData.kt
│ │ ├── UpdaterDialogUIData.kt
│ │ └── UpdaterViewModelData.kt
│ │ ├── theme
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ │ └── utils
│ │ └── AppUpdaterPreviewData.kt
│ └── test
│ └── java
│ └── com
│ └── pouyaheydari
│ └── appupdater
│ └── compose
│ ├── data
│ └── mapper
│ │ └── UpdaterViewModelDataMapperTest.kt
│ └── ui
│ ├── AndroidAppUpdaterScreenTest.kt
│ ├── AndroidAppUpdaterViewModelTest.kt
│ └── components
│ ├── AppUpdaterDialogComponentTest.kt
│ ├── DialogHeaderComponentTest.kt
│ ├── DirectDownloadLinkComponentTest.kt
│ ├── DividerComponentTest.kt
│ ├── SquareStoreItemComponentTest.kt
│ └── UpdateInProgressDialogComponentTest.kt
├── core
├── .gitignore
├── build.gradle.kts
├── gradle.properties
└── src
│ └── main
│ └── java
│ └── com
│ └── pouyaheydari
│ └── appupdater
│ └── core
│ ├── model
│ └── Theme.kt
│ └── utils
│ └── Constants.kt
├── directdownload
├── .gitignore
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ └── com
│ │ │ └── pouyaheydari
│ │ │ └── appupdater
│ │ │ └── directdownload
│ │ │ ├── data
│ │ │ ├── DirectDownloadListItem.kt
│ │ │ └── UpdateInProgressRepositoryImpl.kt
│ │ │ ├── domain
│ │ │ ├── DownloadState.kt
│ │ │ ├── GetDownloadStateUseCase.kt
│ │ │ ├── GetRequestIdUseCase.kt
│ │ │ ├── SetDownloadStateUseCase.kt
│ │ │ ├── SetRequestIdUseCase.kt
│ │ │ └── UpdateInProgressRepository.kt
│ │ │ ├── receiver
│ │ │ └── DownloadFinishedReceiver.kt
│ │ │ └── utils
│ │ │ ├── donwloadapk
│ │ │ ├── APKDownloadManager.kt
│ │ │ ├── APKFileProvider.kt
│ │ │ ├── APKFileProviderImpl.kt
│ │ │ ├── DownloadAPKHelper.kt
│ │ │ └── DownloadManagerRequestCreator.kt
│ │ │ ├── installapk
│ │ │ ├── APKIntentFactory.kt
│ │ │ ├── FileUriProvider.kt
│ │ │ ├── InstallAPKIntent.kt
│ │ │ ├── InstallAPKIntentForM.kt
│ │ │ ├── InstallAPKIntentForNToO.kt
│ │ │ ├── InstallAPKIntentForPAndAbove.kt
│ │ │ └── InstallApkUtil.kt
│ │ │ └── permission
│ │ │ ├── DownloadAPKPermission.kt
│ │ │ ├── DownloadAPKPermissionFactory.kt
│ │ │ ├── DownloadAPKPermissionForOAndBellow.kt
│ │ │ └── DownloadAPKPermissionForPAndAbove.kt
│ └── res
│ │ ├── values
│ │ └── strings.xml
│ │ └── xml
│ │ └── provider_paths.xml
│ └── test
│ └── kotlin
│ └── com
│ └── pouyaheydari
│ └── appupdater
│ └── directdownload
│ ├── data
│ └── UpdateInProgressRepositoryImplTest.kt
│ ├── domain
│ ├── GetDownloadStateUseCaseTest.kt
│ ├── GetRequestIdUseCaseTest.kt
│ ├── SetDownloadStateUseCaseTest.kt
│ └── SetRequestIdUseCaseTest.kt
│ └── utils
│ ├── donwloadapk
│ ├── APKDownloadManagerTest.kt
│ ├── APKFileProviderImplTest.kt
│ ├── DownloadAPKHelperKtTest.kt
│ └── DownloadManagerRequestCreatorTest.kt
│ ├── installapk
│ ├── APKIntentFactoryTest.kt
│ ├── FileUriProviderTest.kt
│ ├── InstallAPKIntentForMTest.kt
│ ├── InstallAPKIntentForNToOTest.kt
│ ├── InstallAPKIntentForPAndAboveTest.kt
│ └── InstallApkUtilKtTest.kt
│ └── permission
│ ├── DownloadAPKPermissionFactoryTest.kt
│ ├── DownloadAPKPermissionForOAndBellowTest.kt
│ ├── DownloadAPKPermissionForPAndAboveTest.kt
│ └── DownloadAPKPermissionForPAndAboveUnknownSourceMethodTest.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── pics
├── header.png
└── icon.png
├── renovate.json
├── settings.gradle.kts
└── store
├── .gitignore
├── build.gradle.kts
├── gradle.properties
└── src
├── androidTest
└── java
│ └── com
│ └── pouyaheydari
│ └── appupdater
│ └── store
│ └── domain
│ ├── StoreManagerTest.kt
│ └── stores
│ ├── AmazonAppStoreTest.kt
│ ├── AptoideTest.kt
│ ├── CafeBazaarStoreTest.kt
│ ├── FDroidTest.kt
│ ├── GooglePlayStoreTest.kt
│ ├── HuaweiAppGalleryTest.kt
│ ├── LenovoAppCenterTest.kt
│ ├── MiGetAppStoreTest.kt
│ ├── MyketStoreTest.kt
│ ├── NineAppsTest.kt
│ ├── OneStoreAppMarketTest.kt
│ ├── OppoAppMarketTest.kt
│ ├── SamsungGalaxyStoreTest.kt
│ ├── TencentStoreTest.kt
│ ├── VAppStoreTest.kt
│ └── ZTEAppCenterTest.kt
├── main
├── java
│ └── com
│ │ └── pouyaheydari
│ │ └── appupdater
│ │ └── store
│ │ └── domain
│ │ ├── AppStoreCallback.kt
│ │ ├── StoreFactory.kt
│ │ ├── StoreIntentBuilder.kt
│ │ ├── StoreListItem.kt
│ │ ├── StoreManager.kt
│ │ └── stores
│ │ ├── AmazonAppStore.kt
│ │ ├── AppStore.kt
│ │ ├── AppStoreType.kt
│ │ ├── Aptoide.kt
│ │ ├── CafeBazaarStore.kt
│ │ ├── FDroid.kt
│ │ ├── GooglePlayStore.kt
│ │ ├── HuaweiAppGallery.kt
│ │ ├── LenovoAppCenter.kt
│ │ ├── MiGetAppStore.kt
│ │ ├── MyketStore.kt
│ │ ├── NineApps.kt
│ │ ├── OneStoreAppMarket.kt
│ │ ├── OppoAppMarket.kt
│ │ ├── SamsungGalaxyStore.kt
│ │ ├── TencentAppStore.kt
│ │ ├── VAppStore.kt
│ │ └── ZTEAppCenter.kt
└── res
│ ├── drawable-nodpi
│ ├── appupdater_ic_amazon_app_store.png
│ ├── appupdater_ic_bazar.png
│ ├── appupdater_ic_cloud.png
│ ├── appupdater_ic_fdroid.png
│ ├── appupdater_ic_galaxy_store.png
│ ├── appupdater_ic_get_app_store.png
│ ├── appupdater_ic_lenovo_app_center.png
│ ├── appupdater_ic_nine_apps.png
│ ├── appupdater_ic_one_store.png
│ ├── appupdater_ic_oppo_app_market.png
│ ├── appupdater_ic_tencent_app_store.png
│ ├── appupdater_ic_v_app_store.png
│ └── appupdater_ic_zte_app_center.png
│ └── drawable
│ ├── appupdater_ic_app_gallery.xml
│ ├── appupdater_ic_aptoide.xml
│ ├── appupdater_ic_google_play.xml
│ └── appupdater_ic_myket.xml
└── test
└── java
└── com
└── pouyaheydari
└── appupdater
└── store
└── domain
├── StoreFactoryTest.kt
└── StoreIntentBuilderTest.kt
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | end_of_line = lf
4 | insert_final_newline = true
5 | indent_style = space
6 | indent_size = 4
7 |
8 | [*.{kt,kts}]
9 | max_line_length = 180
10 | ktlint_function_naming_ignore_when_annotated_with = Composable
11 | ktlint_standard_string-template-indent = disabled
12 | ktlint_standard_multiline-expression-wrapping = disabled
13 | ktlint_standard_blank-line-before-declaration = disabled
14 | ktlint_standard_function-signature = disabled
15 | ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset
16 | ktlint_function_signature_body_expression_wrapping = default
17 | ktlint_standard_backing-property-naming = disabled
18 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: set up JDK 17
17 | uses: actions/setup-java@v4
18 | with:
19 | java-version: '17'
20 | distribution: 'temurin'
21 | cache: gradle
22 |
23 | - name: Grant execute permission for gradlew
24 | run: chmod +x gradlew
25 | - name: Run tests
26 | run: ./gradlew testDebugUnitTest
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | *.jks
11 | *.apk
12 |
13 | # Gradle files
14 | .gradle/
15 | build/
16 |
17 | # Local configuration file (sdk path, etc)
18 | local.properties
19 |
20 | # Log/OS Files
21 | *.log
22 |
23 | # Android Studio generated files and folders
24 | captures/
25 | .externalNativeBuild/
26 | .cxx/
27 | output.json
28 |
29 | # IntelliJ
30 | .idea/
31 | misc.xml
32 | deploymentTargetDropDown.xml
33 | render.experimental.xml
34 |
35 | # Keystore files
36 | *.keystore
37 |
38 | # Google Services (e.g. APIs or Firebase)
39 | google-services.json
40 |
41 | # Android Profiling
42 | *.hprof
43 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidApplication)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | alias(libs.plugins.compose.compiler)
5 | }
6 |
7 | android {
8 | compileSdk = libs.versions.compileSdkVersion.get().toInt()
9 | defaultConfig {
10 | applicationId = "com.pouyaheydari.appupdater.demo"
11 | minSdk = libs.versions.minSdkVersion.get().toInt()
12 | targetSdk = libs.versions.targetSdkVersion.get().toInt()
13 | versionCode = libs.versions.appVersion.get().toInt()
14 | versionName = libs.versions.appVersion.get()
15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
16 | vectorDrawables {
17 | useSupportLibrary = true
18 | }
19 | }
20 | namespace = "com.pouyaheydari.appupdater.demo"
21 |
22 | compileOptions {
23 | sourceCompatibility = JavaVersion.VERSION_17
24 | targetCompatibility = JavaVersion.VERSION_17
25 | }
26 |
27 | kotlinOptions {
28 | jvmTarget = JavaVersion.VERSION_17.toString()
29 | }
30 | buildFeatures {
31 | compose = true
32 | }
33 | packaging {
34 | resources {
35 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
36 | }
37 | }
38 | }
39 |
40 | dependencies {
41 |
42 | // library dependency
43 | implementation(projects.appupdater)
44 | implementation(projects.compose)
45 |
46 | // support dependency
47 | implementation(libs.androidx.appcompat)
48 | implementation(libs.androidx.constraintLayout)
49 |
50 | // compose
51 | val composeBom = platform(libs.androidx.compose.bom)
52 | implementation(composeBom)
53 | implementation(libs.androidx.compose.foundation)
54 | implementation(libs.androidx.compose.ui.tooling)
55 | implementation(libs.androidx.compose.ui.tooling.preview)
56 | implementation(libs.androidx.compose.activity)
57 | implementation(libs.androidx.compose.material)
58 |
59 | // testing dependency
60 | testImplementation(libs.junit4)
61 | androidTestImplementation(libs.androidx.test.junit)
62 | androidTestImplementation(libs.androidx.test.rules)
63 | androidTestImplementation(libs.androidx.test.ui.espresso.core)
64 | androidTestImplementation(composeBom)
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/assets/fonts/vazir.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/app/src/main/assets/fonts/vazir.ttf
--------------------------------------------------------------------------------
/app/src/main/java/com/pouyaheydari/appupdater/demo/ui/compose/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.demo.ui.compose.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.darkColorScheme
6 | import androidx.compose.material3.lightColorScheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.graphics.Color
9 |
10 | private val DarkColorScheme = darkColorScheme(
11 | primary = Color.Black,
12 | )
13 |
14 | private val LightColorScheme = lightColorScheme(
15 | primary = Color.Black,
16 | )
17 |
18 | @Composable
19 | fun AndroidAppUpdaterTheme(
20 | darkTheme: Boolean = isSystemInDarkTheme(),
21 | content: @Composable () -> Unit,
22 | ) {
23 | val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
24 | MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pouyaheydari/appupdater/demo/ui/compose/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.demo.ui.compose.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | internal val Typography = Typography(
10 | bodyLarge = TextStyle(
11 | fontFamily = FontFamily.Default,
12 | fontWeight = FontWeight.Normal,
13 | fontSize = 16.sp,
14 | lineHeight = 24.sp,
15 | letterSpacing = 0.5.sp,
16 | ),
17 | )
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pouyaheydari/appupdater/demo/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.demo.utils
2 |
3 | internal const val APK_URL = "https://cafebazaar.ir/download/bazaar.apk"
4 | internal const val SAMPLE_PACKAGE_NAME = "com.tencent.mm"
5 | internal const val FDROID_SAMPLE_PACKAGE_NAME = "de.storchp.fdroidbuildstatus"
6 | internal const val GET_APP_SAMPLE_PACKAGE_NAME = "com.opera.browser"
7 | internal const val ONE_STORE_SAMPLE_PACKAGE_NAME = "com.kakao.talk"
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
24 |
25 |
39 |
40 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Android App Updater
3 | Kotlin DSL Sample
4 | Kotlin Sample
5 | New Update!
6 | Lots of new features! Update right now
7 | Direct Download %s
8 | Play
9 | Bazaar
10 | Myket
11 | App Gallery
12 | Galaxy Store
13 | Amazon App Store
14 | Compose Sample
15 | Aptoide
16 | FDroid
17 | Mi GetApp
18 | One Store
19 | Oppo App Market
20 | V-App Store
21 | Nine Apps
22 | Tencent AppStore
23 | ZTE App Store
24 | Lenovo App Center
25 | Show Compose Dialog
26 | %s Store is not installed
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/appupdater/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/appupdater/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
2 |
3 | plugins {
4 | alias(libs.plugins.androidLibrary)
5 | alias(libs.plugins.jetbrainsKotlinAndroid)
6 | alias(libs.plugins.maven.publish)
7 | }
8 | android {
9 | compileSdk = libs.versions.compileSdkVersion.get().toInt()
10 | defaultConfig {
11 | minSdk = libs.versions.minSdkVersion.get().toInt()
12 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
13 | }
14 | compileOptions {
15 | sourceCompatibility = JavaVersion.VERSION_17
16 | targetCompatibility = JavaVersion.VERSION_17
17 | }
18 |
19 | kotlinOptions {
20 | jvmTarget = JavaVersion.VERSION_17.toString()
21 | }
22 | buildFeatures {
23 | viewBinding = true
24 | }
25 | namespace = "com.pouyaheydari.appupdater.main"
26 | }
27 |
28 | mavenPublishing {
29 | configure(
30 | AndroidSingleVariantLibrary(
31 | variant = "release",
32 | sourcesJar = true,
33 | publishJavadocJar = false,
34 | )
35 | )
36 | }
37 |
38 | dependencies {
39 |
40 | api(projects.store)
41 | api(projects.directdownload)
42 |
43 | // support dependency
44 | implementation(libs.androidx.appcompat)
45 | implementation(libs.androidx.constraintLayout)
46 | implementation(libs.androidx.recyclerView)
47 | implementation(libs.kotlinx.coroutines)
48 | implementation(libs.androidx.fragment)
49 |
50 | // testing dependency
51 | testImplementation(libs.junit4)
52 | testImplementation(libs.mockito.kotlin)
53 | testImplementation(libs.turbine)
54 | testImplementation (libs.kotlinx.coroutines.test)
55 | androidTestImplementation(libs.androidx.test.junit)
56 | androidTestImplementation(libs.androidx.test.rules)
57 | androidTestImplementation(libs.androidx.test.ui.espresso.core)
58 | }
59 |
--------------------------------------------------------------------------------
/appupdater/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=main
2 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/data/mapper/SelectedThemeMapper.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.data.mapper
2 |
3 | import android.content.Context
4 | import android.content.res.Configuration
5 | import com.pouyaheydari.appupdater.core.model.Theme
6 | import com.pouyaheydari.appupdater.core.model.Theme.DARK
7 | import com.pouyaheydari.appupdater.core.model.Theme.LIGHT
8 | import com.pouyaheydari.appupdater.core.model.Theme.SYSTEM_DEFAULT
9 | import com.pouyaheydari.appupdater.main.ui.model.UserSelectedTheme
10 |
11 | internal fun mapToSelectedTheme(theme: Theme, context: Context): UserSelectedTheme = when (theme) {
12 | LIGHT -> UserSelectedTheme.LIGHT
13 | DARK -> UserSelectedTheme.DARK
14 | SYSTEM_DEFAULT -> if (context.isSystemInDarkMode()) UserSelectedTheme.DARK else UserSelectedTheme.LIGHT
15 | }
16 |
17 | private fun Context.isSystemInDarkMode(): Boolean =
18 | resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
19 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/dsl/DSLUtils.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.dsl
2 |
3 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
4 | import com.pouyaheydari.appupdater.main.ui.AppUpdaterDialog
5 | import com.pouyaheydari.appupdater.main.ui.model.UpdaterDialogData
6 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
7 |
8 | /**
9 | * This inline function helps building stores in DSL way
10 | */
11 | inline fun store(block: StoreListItem.() -> Unit): StoreListItem = StoreListItem().apply(block)
12 |
13 | /**
14 | * This inline function helps building direct download links in DSL way
15 | */
16 | inline fun directDownload(block: DirectDownloadListItem.() -> Unit): DirectDownloadListItem = DirectDownloadListItem().apply(block)
17 |
18 | /**
19 | * This inline function helps building UpdateDialog in DSL way
20 | */
21 | inline fun updateDialogBuilder(block: UpdaterDialogData.() -> Unit): AppUpdaterDialog =
22 | AppUpdaterDialog.getInstance(UpdaterDialogData().apply(block))
23 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.pouyaheydari.appupdater.directdownload.domain.DownloadState
6 | import com.pouyaheydari.appupdater.directdownload.domain.GetDownloadStateUseCase
7 | import com.pouyaheydari.appupdater.directdownload.domain.SetDownloadStateUseCase
8 | import com.pouyaheydari.appupdater.main.ui.model.DialogScreenIntents
9 | import com.pouyaheydari.appupdater.main.ui.model.DialogScreenStates
10 | import com.pouyaheydari.appupdater.main.utils.ErrorCallbackHolder
11 | import com.pouyaheydari.appupdater.main.utils.TypefaceHolder
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.collectLatest
14 | import kotlinx.coroutines.launch
15 |
16 | internal class AppUpdaterViewModel(
17 | private val isUpdateInProgressUseCase: GetDownloadStateUseCase,
18 | private val setDownloadStateUseCase: SetDownloadStateUseCase
19 | ) : ViewModel() {
20 | val screenState = MutableStateFlow(DialogScreenStates.HideUpdateInProgress)
21 |
22 | fun handleIntent(intent: DialogScreenIntents) {
23 | when (intent) {
24 | is DialogScreenIntents.OnDirectLinkClicked -> screenState.value = DialogScreenStates.DownloadApk(intent.item.url)
25 | is DialogScreenIntents.OnStoreClicked -> screenState.value = DialogScreenStates.OpenStore(intent.item.store)
26 | DialogScreenIntents.OnStoreOpened -> screenState.value = DialogScreenStates.Empty
27 | DialogScreenIntents.OnErrorCallbackExecuted -> screenState.value = DialogScreenStates.Empty
28 | DialogScreenIntents.OnApkDownloadRequested -> screenState.value = DialogScreenStates.Empty
29 | DialogScreenIntents.OnApkDownloadStarted -> {
30 | setUpdateInProgress()
31 | observeUpdateInProgressStatus()
32 | }
33 |
34 | is DialogScreenIntents.OnOpeningStoreFailed ->
35 | screenState.value = DialogScreenStates.ExecuteErrorCallback(intent.store.getUserReadableName())
36 |
37 | DialogScreenIntents.OnApkInstallationStarted -> screenState.value = DialogScreenStates.Empty
38 | }
39 | }
40 |
41 | private fun setUpdateInProgress() {
42 | viewModelScope.launch {
43 | setDownloadStateUseCase(DownloadState.Downloading)
44 | }
45 | }
46 |
47 | private fun observeUpdateInProgressStatus() {
48 | viewModelScope.launch {
49 | isUpdateInProgressUseCase().collectLatest { downloadState ->
50 | screenState.value = when (downloadState) {
51 | is DownloadState.Downloaded ->
52 | DialogScreenStates.InstallApk(downloadState.apk)
53 |
54 | is DownloadState.Downloading ->
55 | DialogScreenStates.ShowUpdateInProgress
56 | }
57 | }
58 | }
59 | }
60 |
61 | override fun onCleared() {
62 | TypefaceHolder.clear()
63 | ErrorCallbackHolder.clear()
64 | super.onCleared()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.pouyaheydari.appupdater.directdownload.data.UpdateInProgressRepositoryImpl
6 | import com.pouyaheydari.appupdater.directdownload.domain.GetDownloadStateUseCase
7 | import com.pouyaheydari.appupdater.directdownload.domain.SetDownloadStateUseCase
8 |
9 | internal class AppUpdaterViewModelFactory(
10 | private val getDownloadStateUseCase: GetDownloadStateUseCase = GetDownloadStateUseCase(UpdateInProgressRepositoryImpl),
11 | private val setDownloadStateUseCase: SetDownloadStateUseCase = SetDownloadStateUseCase(UpdateInProgressRepositoryImpl)
12 | ) : ViewModelProvider.Factory {
13 | @Suppress("UNCHECKED_CAST")
14 | override fun create(modelClass: Class): T {
15 | return AppUpdaterViewModel(getDownloadStateUseCase, setDownloadStateUseCase) as T
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/DirectRecyclerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui.adapters
2 |
3 | import android.graphics.Typeface
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
8 | import com.pouyaheydari.appupdater.main.databinding.DownloadDirectItemBinding
9 |
10 | internal class DirectRecyclerAdapter(
11 | private val list: List,
12 | private val typeface: Typeface?,
13 | private val listener: (DirectDownloadListItem) -> Unit,
14 | ) : RecyclerView.Adapter() {
15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SoresViewHolder =
16 | DownloadDirectItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
17 | .run { SoresViewHolder(this) }
18 |
19 | override fun getItemCount(): Int = list.size
20 |
21 | override fun onBindViewHolder(holder: SoresViewHolder, position: Int) = holder.onBind(list[position])
22 |
23 | inner class SoresViewHolder(private val binding: DownloadDirectItemBinding) : RecyclerView.ViewHolder(binding.root) {
24 | fun onBind(item: DirectDownloadListItem) {
25 | val txtDirect = binding.txtDirect
26 | txtDirect.text = item.title
27 | typeface?.let { txtDirect.typeface = it }
28 | binding.root.setOnClickListener { listener(item) }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/adapters/StoresRecyclerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui.adapters
2 |
3 | import android.graphics.Typeface
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.core.content.ContextCompat
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.pouyaheydari.appupdater.main.R
9 | import com.pouyaheydari.appupdater.main.databinding.DownloadStoresItemBinding
10 | import com.pouyaheydari.appupdater.main.ui.model.UserSelectedTheme
11 | import com.pouyaheydari.appupdater.main.ui.model.UserSelectedTheme.DARK
12 | import com.pouyaheydari.appupdater.main.ui.model.UserSelectedTheme.LIGHT
13 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
14 |
15 | internal class StoresRecyclerAdapter(
16 | private val list: List,
17 | private val theme: UserSelectedTheme,
18 | private val typeface: Typeface?,
19 | private val listener: (StoreListItem) -> Unit,
20 | ) : RecyclerView.Adapter() {
21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
22 | DownloadStoresItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
23 | .run { SoresViewHolder(this) }
24 |
25 | override fun getItemCount(): Int = list.size
26 |
27 | override fun onBindViewHolder(holder: SoresViewHolder, position: Int) = holder.onBind(list[position])
28 |
29 | inner class SoresViewHolder(private val binding: DownloadStoresItemBinding) : RecyclerView.ViewHolder(binding.root) {
30 | fun onBind(item: StoreListItem) {
31 | val txtStoreTitle = binding.txtStoreTitle
32 | val imgStore = binding.imgStore
33 | val textColor = when (theme) {
34 | LIGHT -> R.color.appupdater_text_colors
35 | DARK -> R.color.appupdater_text_colors_dark
36 | }
37 | txtStoreTitle.setTextColor(ContextCompat.getColor(binding.root.context, textColor))
38 | txtStoreTitle.text = item.title
39 | typeface?.let { txtStoreTitle.typeface = it }
40 | imgStore.setImageResource(item.icon)
41 | binding.root.setOnClickListener { listener(item) }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/DialogScreenIntents.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui.model
2 |
3 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
4 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
5 | import com.pouyaheydari.appupdater.store.domain.stores.AppStore
6 |
7 | internal sealed interface DialogScreenIntents {
8 | data class OnStoreClicked(val item: StoreListItem) : DialogScreenIntents
9 | data class OnDirectLinkClicked(val item: DirectDownloadListItem) : DialogScreenIntents
10 | data class OnOpeningStoreFailed(val store: AppStore) : DialogScreenIntents
11 | data object OnStoreOpened : DialogScreenIntents
12 | data object OnErrorCallbackExecuted : DialogScreenIntents
13 | data object OnApkDownloadStarted : DialogScreenIntents
14 | data object OnApkDownloadRequested : DialogScreenIntents
15 | data object OnApkInstallationStarted : DialogScreenIntents
16 | }
17 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/DialogScreenStates.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui.model
2 |
3 | import com.pouyaheydari.appupdater.store.domain.stores.AppStore
4 | import java.io.File
5 |
6 | internal sealed interface DialogScreenStates {
7 | data object Empty : DialogScreenStates
8 | data object ShowUpdateInProgress : DialogScreenStates
9 | data object HideUpdateInProgress : DialogScreenStates
10 | data class OpenStore(val store: AppStore) : DialogScreenStates
11 | data class DownloadApk(val apkUrl: String) : DialogScreenStates
12 | data class ExecuteErrorCallback(val storeName: String) : DialogScreenStates
13 | data class InstallApk(val apk: File) : DialogScreenStates
14 | }
15 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterDialogData.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui.model
2 |
3 | import android.graphics.Typeface
4 | import com.pouyaheydari.appupdater.core.model.Theme
5 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
6 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
7 |
8 | /**
9 | * This model is used to pass the data to dialog fragment via bundles
10 | */
11 | data class UpdaterDialogData(
12 | var title: String = "",
13 | var description: String = "",
14 | var storeList: List = listOf(),
15 | var directDownloadList: List = listOf(),
16 | var isForceUpdate: Boolean = false,
17 | var typeface: Typeface? = null,
18 | var errorWhileOpeningStoreCallback: ((String) -> Unit)? = null,
19 | var theme: Theme = Theme.SYSTEM_DEFAULT,
20 | )
21 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UpdaterFragmentModel.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui.model
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.core.model.Theme
6 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
7 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
8 |
9 | /**
10 | * This model is used to pass the data to dialog fragment via bundles
11 | */
12 | internal data class UpdaterFragmentModel(
13 | var title: String = "",
14 | var description: String = "",
15 | var storeList: List = listOf(),
16 | var directDownloadList: List = listOf(),
17 | var isForceUpdate: Boolean = false,
18 | var theme: Theme = Theme.SYSTEM_DEFAULT,
19 | ) : Parcelable {
20 | constructor(parcel: Parcel) : this(
21 | parcel.readString().orEmpty(),
22 | parcel.readString().orEmpty(),
23 | parcel.createTypedArrayList(StoreListItem).orEmpty(),
24 | parcel.createTypedArrayList(DirectDownloadListItem).orEmpty(),
25 | parcel.readByte() != 0.toByte(),
26 | Theme.entries[parcel.readInt()],
27 | )
28 |
29 | override fun writeToParcel(parcel: Parcel, flags: Int) {
30 | parcel.writeString(title)
31 | parcel.writeString(description)
32 | parcel.writeTypedList(storeList)
33 | parcel.writeByte(if (isForceUpdate) 1 else 0)
34 | parcel.writeInt(theme.ordinal)
35 | }
36 |
37 | override fun describeContents(): Int {
38 | return 0
39 | }
40 |
41 | companion object CREATOR : Parcelable.Creator {
42 | val EMPTY = UpdaterFragmentModel()
43 | override fun createFromParcel(parcel: Parcel): UpdaterFragmentModel {
44 | return UpdaterFragmentModel(parcel)
45 | }
46 |
47 | override fun newArray(size: Int): Array {
48 | return arrayOfNulls(size)
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/UserSelectedTheme.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.ui.model
2 |
3 | internal enum class UserSelectedTheme { LIGHT, DARK }
4 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/EnumBundleExtension.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.utils
2 |
3 | import android.os.Bundle
4 |
5 | /**
6 | * A function to put enums in bundles
7 | */
8 | internal fun Bundle.putEnum(key: String, enum: Enum<*>) {
9 | this.putInt(key, enum.ordinal)
10 | }
11 |
12 | /**
13 | * A function to retrieve enums from bundles
14 | */
15 | internal inline fun > Bundle.getEnum(key: String): T {
16 | return enumValues()[getInt(key, 0)]
17 | }
18 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/ErrorCallbackHolder.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.utils
2 |
3 | import android.graphics.Typeface
4 |
5 | /**
6 | * Holds an instance of [Typeface] to be used in dialogs while being shown.
7 | */
8 | internal object ErrorCallbackHolder {
9 | var callback: ((String) -> Unit)? = null
10 |
11 | fun clear() {
12 | callback = null
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/GetDialogWidth.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.utils
2 |
3 | import android.content.res.Resources
4 | import android.graphics.Rect
5 |
6 | /**
7 | * Calculates dialog's widths to only cover a certain percentage of the screen
8 | *
9 | * @param percentage of the device to be covered by the dialog
10 | */
11 | internal fun getDialogWidth(percentage: Int = 90): Int {
12 | val percent = percentage.toFloat() / 100
13 | val dm = Resources.getSystem().displayMetrics
14 | val rect = dm.run { Rect(0, 0, widthPixels, heightPixels) }
15 | return (rect.width() * percent).toInt()
16 | }
17 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/ParcelableCompatApi.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.utils
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import android.os.Parcelable
6 |
7 | /**
8 | * This extension function helps with using parcelables,
9 | * as [Bundle.getParcelable()] is deprecated in [Build.VERSION_CODES.TIRAMISU].
10 | */
11 | internal inline fun Bundle.parcelable(key: String): T? = when {
12 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getParcelable(key, T::class.java)
13 | else -> {
14 | @Suppress("DEPRECATION")
15 | getParcelable(key) as? T
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/appupdater/src/main/java/com/pouyaheydari/appupdater/main/utils/TypefaceHolder.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.utils
2 |
3 | import android.graphics.Typeface
4 |
5 | /**
6 | * Holds an instance of [Typeface] to be used in dialogs while being shown.
7 | */
8 | internal object TypefaceHolder {
9 | var typeface: Typeface? = null
10 |
11 | fun clear() {
12 | typeface = null
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/appupdater/src/main/res/drawable/dialog_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/appupdater/src/main/res/drawable/dialog_background_dark.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/appupdater/src/main/res/drawable/update_in_progress_dialog_background_dark.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/appupdater/src/main/res/layout/download_direct_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
--------------------------------------------------------------------------------
/appupdater/src/main/res/layout/download_stores_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
21 |
22 |
34 |
35 |
--------------------------------------------------------------------------------
/appupdater/src/main/res/layout/fragment_update_in_progress_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
22 |
23 |
36 |
37 |
50 |
51 |
--------------------------------------------------------------------------------
/appupdater/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #191919
4 | #2ea3cc
5 | #4d4d4d
6 | #ededed
7 |
8 |
--------------------------------------------------------------------------------
/appupdater/src/test/java/com/pouyaheydari/appupdater/main/data/mapper/SelectedThemeMapperTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.main.data.mapper
2 |
3 | import android.content.Context
4 | import android.content.res.Configuration
5 | import android.content.res.Resources
6 | import com.pouyaheydari.appupdater.core.model.Theme
7 | import com.pouyaheydari.appupdater.main.ui.model.UserSelectedTheme
8 | import org.junit.Assert.assertEquals
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.mockito.Mockito.mock
12 | import org.mockito.junit.MockitoJUnitRunner
13 | import org.mockito.kotlin.whenever
14 |
15 | @RunWith(MockitoJUnitRunner::class)
16 | class SelectedThemeMapperTest {
17 |
18 | private val context: Context = mock()
19 | private val resources: Resources = mock()
20 |
21 | @Test
22 | fun `mapToSelectedTheme should return LIGHT when theme is LIGHT`() {
23 | val result = mapToSelectedTheme(Theme.LIGHT, context)
24 | assertEquals(UserSelectedTheme.LIGHT, result)
25 | }
26 |
27 | @Test
28 | fun `mapToSelectedTheme should return DARK when theme is DARK`() {
29 | val result = mapToSelectedTheme(Theme.DARK, context)
30 | assertEquals(UserSelectedTheme.DARK, result)
31 | }
32 |
33 | @Test
34 | fun `mapToSelectedTheme should return DARK when system is in dark mode`() {
35 | whenever(context.resources).thenReturn(resources)
36 | whenever(resources.configuration).thenReturn(Configuration().apply {
37 | uiMode = Configuration.UI_MODE_NIGHT_YES
38 | })
39 |
40 | val result = mapToSelectedTheme(Theme.SYSTEM_DEFAULT, context)
41 | assertEquals(UserSelectedTheme.DARK, result)
42 | }
43 |
44 | @Test
45 | fun `mapToSelectedTheme should return LIGHT when system is in light mode`() {
46 | whenever(context.resources).thenReturn(resources)
47 | whenever(resources.configuration).thenReturn(Configuration().apply {
48 | uiMode = Configuration.UI_MODE_NIGHT_NO
49 | })
50 |
51 | val result = mapToSelectedTheme(Theme.SYSTEM_DEFAULT, context)
52 | assertEquals(UserSelectedTheme.LIGHT, result)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidApplication) apply false
3 | alias(libs.plugins.androidLibrary) apply false
4 | alias(libs.plugins.jetbrainsKotlinAndroid) apply false
5 | alias(libs.plugins.kotlin.jvm) apply false
6 | alias(libs.plugins.compose.compiler) apply false
7 | alias(libs.plugins.maven.publish) apply false
8 | }
9 |
--------------------------------------------------------------------------------
/compose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/compose/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | alias(libs.plugins.compose.compiler)
5 | alias(libs.plugins.maven.publish)
6 | }
7 |
8 | android {
9 | compileSdk = libs.versions.compileSdkVersion.get().toInt()
10 | defaultConfig {
11 | minSdk = libs.versions.minSdkVersion.get().toInt()
12 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
13 | vectorDrawables {
14 | useSupportLibrary = true
15 | }
16 | }
17 |
18 | namespace = "com.pouyaheydari.appupdater.compose"
19 |
20 | testOptions.unitTests.isIncludeAndroidResources = true
21 |
22 | compileOptions {
23 | sourceCompatibility = JavaVersion.VERSION_17
24 | targetCompatibility = JavaVersion.VERSION_17
25 | }
26 | kotlinOptions {
27 | jvmTarget = JavaVersion.VERSION_17.toString()
28 | }
29 | buildFeatures {
30 | compose = true
31 | }
32 | packaging {
33 | resources {
34 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
35 | }
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | api(projects.store)
42 | api(projects.directdownload)
43 |
44 | // compose
45 | val composeBom = platform(libs.androidx.compose.bom)
46 | implementation(composeBom)
47 | implementation(libs.androidx.compose.foundation)
48 | implementation(libs.androidx.compose.viewModel)
49 | implementation(libs.androidx.compose.material)
50 | implementation(libs.androidx.compose.ui.tooling.preview)
51 | implementation(libs.androidx.lifecycle)
52 |
53 | // testing and preview
54 | debugImplementation(libs.androidx.compose.ui.test.manifest)
55 | debugImplementation(libs.androidx.compose.ui.tooling)
56 | testImplementation(libs.junit4)
57 | testImplementation(libs.androidx.compose.ui.test.junit)
58 | testImplementation(libs.mockito.kotlin)
59 | testImplementation(libs.roboelectric)
60 | testImplementation(libs.turbine)
61 | androidTestImplementation(libs.androidx.test.rules)
62 | androidTestImplementation(libs.androidx.test.junit)
63 | androidTestImplementation(libs.androidx.test.ui.espresso.core)
64 | androidTestImplementation(libs.androidx.compose.ui.test.junit)
65 | androidTestImplementation(libs.mockito.android)
66 | androidTestImplementation(libs.mockito.kotlin)
67 | androidTestImplementation(libs.androidx.uiautomator)
68 | }
69 |
--------------------------------------------------------------------------------
/compose/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=compose
2 |
--------------------------------------------------------------------------------
/compose/src/androidTest/java/com/pouyaheydari/appupdater/compose/ui/components/AppUpdaterDialogComponentAndroidTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
4 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
5 | import androidx.test.uiautomator.UiDevice
6 | import com.pouyaheydari.appupdater.compose.ComposeTestActivity
7 | import com.pouyaheydari.appupdater.compose.ui.models.UpdaterDialogUIData
8 | import org.junit.Rule
9 | import org.junit.Test
10 | import org.mockito.kotlin.mock
11 | import org.mockito.kotlin.verify
12 |
13 | internal class AppUpdaterDialogComponentAndroidTest {
14 | @get:Rule
15 | val composeTestRule = createAndroidComposeRule()
16 |
17 | @Test
18 | fun test_wheneverDialogIsDismissed_thenCorrectCallbackIsBeingCalled() {
19 | val onDismissRequest: () -> Unit = mock()
20 | val dialogContent = UpdaterDialogUIData(onDismissRequested = onDismissRequest)
21 | composeTestRule.setContent {
22 | AppUpdaterDialogComponent(dialogContent = dialogContent)
23 | }
24 |
25 | // https://issuetracker.google.com/issues/229759201?pli=1
26 | UiDevice.getInstance(getInstrumentation()).pressBack()
27 |
28 | verify(onDismissRequest).invoke()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/compose/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/compose/src/debug/java/com/pouyaheydari/appupdater/compose/ComposeTestActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose
2 |
3 | import androidx.activity.ComponentActivity
4 |
5 | class ComposeTestActivity : ComponentActivity()
6 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/data/mapper/UpdaterDialogUIMapper.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.data.mapper
2 |
3 | import com.pouyaheydari.appupdater.compose.ui.models.DialogHeaderModel
4 | import com.pouyaheydari.appupdater.compose.ui.models.UpdaterDialogUIData
5 | import com.pouyaheydari.appupdater.compose.ui.models.UpdaterViewModelData
6 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
7 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
8 |
9 | internal object UpdaterDialogUIMapper {
10 | fun map(
11 | viewModelData: UpdaterViewModelData,
12 | onStoreClickListener: (StoreListItem) -> Unit,
13 | onDirectLinkClickListener: (DirectDownloadListItem) -> Unit,
14 | ): UpdaterDialogUIData =
15 | with(viewModelData) {
16 | UpdaterDialogUIData(
17 | dialogHeader = DialogHeaderModel(dialogTitle, dialogDescription),
18 | dividerText = dividerText,
19 | directDownloadList = viewModelData.directDownloadList,
20 | storeList = viewModelData.storeList,
21 | shouldShowDividers = shouldShowStoresDivider(viewModelData.directDownloadList, viewModelData.storeList),
22 | onDismissRequested = onDismissRequested,
23 | onStoreClickListener = onStoreClickListener,
24 | onDirectLinkClickListener = onDirectLinkClickListener,
25 | )
26 | }
27 |
28 | private fun shouldShowStoresDivider(directDownloadList: List, storeList: List) =
29 | directDownloadList.isNotEmpty() && storeList.isNotEmpty()
30 | }
31 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/data/mapper/UpdaterViewModelDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.data.mapper
2 |
3 | import com.pouyaheydari.appupdater.compose.ui.models.UpdaterDialogData
4 | import com.pouyaheydari.appupdater.compose.ui.models.UpdaterViewModelData
5 |
6 | internal object UpdaterViewModelDataMapper {
7 | fun map(dialogData: UpdaterDialogData): UpdaterViewModelData = with(dialogData) {
8 | UpdaterViewModelData(
9 | dialogTitle = dialogTitle,
10 | dialogDescription = dialogDescription,
11 | dividerText = dividerText,
12 | storeList = storeList,
13 | directDownloadList = directDownloadList,
14 | onDismissRequested = onDismissRequested,
15 | theme = theme,
16 | )
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.pouyaheydari.appupdater.compose.data.mapper.UpdaterViewModelDataMapper
6 | import com.pouyaheydari.appupdater.compose.ui.models.UpdaterDialogData
7 | import com.pouyaheydari.appupdater.directdownload.data.UpdateInProgressRepositoryImpl
8 | import com.pouyaheydari.appupdater.directdownload.domain.GetDownloadStateUseCase
9 | import com.pouyaheydari.appupdater.directdownload.domain.SetDownloadStateUseCase
10 |
11 | internal class AndroidAppUpdaterViewModelFactory(
12 | private val dialogData: UpdaterDialogData,
13 | private val getDownloadStateUseCase: GetDownloadStateUseCase = GetDownloadStateUseCase(UpdateInProgressRepositoryImpl),
14 | private val setDownloadStateUseCase: SetDownloadStateUseCase = SetDownloadStateUseCase(UpdateInProgressRepositoryImpl)
15 | ) : ViewModelProvider.Factory {
16 | @Suppress("UNCHECKED_CAST")
17 | override fun create(modelClass: Class): T {
18 | val viewModelData = UpdaterViewModelDataMapper.map(dialogData)
19 | return AndroidAppUpdaterViewModel(viewModelData, getDownloadStateUseCase, setDownloadStateUseCase) as T
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/components/DirectDownloadLinkComponent.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import android.graphics.Typeface
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.text.font.FontFamily
10 | import androidx.compose.ui.text.style.TextAlign
11 | import androidx.compose.ui.tooling.preview.PreviewFontScale
12 | import androidx.compose.ui.tooling.preview.PreviewLightDark
13 | import androidx.compose.ui.tooling.preview.PreviewScreenSizes
14 | import com.pouyaheydari.appupdater.compose.ui.theme.AndroidAppUpdaterTheme
15 | import com.pouyaheydari.appupdater.compose.ui.theme.Blue
16 |
17 | @Composable
18 | internal fun DirectDownloadLinkComponent(
19 | modifier: Modifier = Modifier,
20 | title: String = "",
21 | typeface: Typeface? = null,
22 | onClickListener: () -> Unit = {},
23 | ) {
24 | Text(
25 | modifier = modifier.clickable { onClickListener() },
26 | text = title,
27 | textAlign = TextAlign.Center,
28 | color = Blue,
29 | style = MaterialTheme.typography.bodyLarge,
30 | fontFamily = typeface?.let { FontFamily(it) },
31 | )
32 | }
33 |
34 | @PreviewLightDark
35 | @PreviewFontScale
36 | @PreviewScreenSizes
37 | @Composable
38 | private fun Preview() {
39 | AndroidAppUpdaterTheme {
40 | DirectDownloadLinkComponent(title = "Direct Link 1")
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/components/DividerComponent.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import android.graphics.Typeface
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.HorizontalDivider
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.text.font.FontFamily
15 | import androidx.compose.ui.tooling.preview.PreviewFontScale
16 | import androidx.compose.ui.tooling.preview.PreviewLightDark
17 | import androidx.compose.ui.tooling.preview.PreviewScreenSizes
18 | import androidx.compose.ui.unit.dp
19 | import com.pouyaheydari.appupdater.compose.ui.theme.AndroidAppUpdaterTheme
20 | import com.pouyaheydari.appupdater.directdownload.R as directDownloadR
21 |
22 | /**
23 | * Shows a divider between direct download links and stores
24 | */
25 | @Composable
26 | internal fun DividerComponent(
27 | modifier: Modifier = Modifier,
28 | dividerText: String = "",
29 | typeface: Typeface? = null,
30 | ) {
31 | Row(
32 | verticalAlignment = Alignment.CenterVertically,
33 | modifier = modifier.fillMaxWidth(),
34 | ) {
35 | HorizontalDivider(
36 | color = MaterialTheme.colorScheme.background,
37 | modifier = Modifier
38 | .weight(1F)
39 | .padding(start = 16.dp, end = 8.dp),
40 | )
41 |
42 | Text(text = dividerText, fontFamily = typeface?.let { FontFamily(it) })
43 |
44 | HorizontalDivider(
45 | color = MaterialTheme.colorScheme.background,
46 | modifier = Modifier
47 | .weight(1F)
48 | .padding(start = 8.dp, end = 16.dp),
49 | )
50 | }
51 | }
52 |
53 | @PreviewLightDark
54 | @PreviewFontScale
55 | @PreviewScreenSizes
56 | @Composable
57 | private fun Preview() {
58 | AndroidAppUpdaterTheme {
59 | DividerComponent(dividerText = stringResource(id = directDownloadR.string.appupdater_or))
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/components/SquareStoreItemComponent.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import android.graphics.Typeface
4 | import androidx.annotation.DrawableRes
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment.Companion.CenterHorizontally
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.testTag
16 | import androidx.compose.ui.res.painterResource
17 | import androidx.compose.ui.text.font.FontFamily
18 | import androidx.compose.ui.text.style.TextOverflow
19 | import androidx.compose.ui.tooling.preview.PreviewFontScale
20 | import androidx.compose.ui.tooling.preview.PreviewLightDark
21 | import androidx.compose.ui.tooling.preview.PreviewScreenSizes
22 | import androidx.compose.ui.unit.dp
23 | import com.pouyaheydari.appupdater.compose.ui.theme.AndroidAppUpdaterTheme
24 | import com.pouyaheydari.appupdater.store.R as storeR
25 |
26 | @Composable
27 | internal fun SquareStoreItemComponent(
28 | modifier: Modifier = Modifier,
29 | title: String = "",
30 | @DrawableRes icon: Int = storeR.drawable.appupdater_ic_google_play,
31 | typeface: Typeface? = null,
32 | onClickListener: () -> Unit = {},
33 | ) {
34 | Column(
35 | modifier = modifier.clickable { onClickListener() },
36 | horizontalAlignment = CenterHorizontally,
37 | ) {
38 | Image(
39 | modifier = Modifier
40 | .size(64.dp)
41 | .testTag(icon.toString()),
42 | painter = painterResource(id = icon),
43 | contentDescription = null,
44 | )
45 |
46 | Text(
47 | modifier = Modifier.padding(top = 8.dp),
48 | text = title,
49 | maxLines = 1,
50 | overflow = TextOverflow.Ellipsis,
51 | style = MaterialTheme.typography.bodyMedium,
52 | color = MaterialTheme.colorScheme.onSurface,
53 | fontFamily = typeface?.let { FontFamily(it) },
54 | )
55 | }
56 | }
57 |
58 | @PreviewLightDark
59 | @PreviewFontScale
60 | @PreviewScreenSizes
61 | @Composable
62 | private fun Preview() {
63 | AndroidAppUpdaterTheme {
64 | SquareStoreItemComponent(title = "Google Play", icon = storeR.drawable.appupdater_ic_google_play)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/DialogHeaderModel.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.models
2 |
3 | import androidx.annotation.DrawableRes
4 | import com.pouyaheydari.appupdater.store.R as storeR
5 |
6 | internal data class DialogHeaderModel(
7 | val dialogTitle: String = "",
8 | val dialogDescription: String = "",
9 | @DrawableRes val dialogIcon: Int = storeR.drawable.appupdater_ic_cloud,
10 | )
11 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/DialogScreenIntents.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.models
2 |
3 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
4 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
5 | import com.pouyaheydari.appupdater.store.domain.stores.AppStore
6 |
7 | internal sealed interface DialogScreenIntents {
8 | data class OnStoreClicked(val item: StoreListItem) : DialogScreenIntents
9 | data class OnDirectLinkClicked(val item: DirectDownloadListItem) : DialogScreenIntents
10 | data object OnStoreOpened : DialogScreenIntents
11 | data class OnOpeningStoreFailed(val store: AppStore) : DialogScreenIntents
12 | data object OnErrorCallbackExecuted : DialogScreenIntents
13 | data object OnApkDownloadStarted : DialogScreenIntents
14 | data object OnApkDownloadRequested : DialogScreenIntents
15 | data object OnApkInstallationStarted : DialogScreenIntents
16 | }
17 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/DialogScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.models
2 |
3 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
4 | import com.pouyaheydari.appupdater.store.domain.stores.AppStore
5 | import com.pouyaheydari.appupdater.store.domain.stores.AppStoreType
6 | import java.io.File
7 |
8 | internal data class DialogScreenState(
9 | val shouldShowDialog: Boolean = true,
10 | val errorWhileOpeningStore: ErrorWhileOpeningStore = ErrorWhileOpeningStore(),
11 | val selectedStore: AppStore = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, ""),
12 | val shouldOpenStore: Boolean = false,
13 | val dialogContent: UpdaterDialogUIData = UpdaterDialogUIData(),
14 | val downloadState: ApkDownloadState = ApkDownloadState(),
15 | )
16 |
17 | internal data class ErrorWhileOpeningStore(
18 | val shouldNotifyCaller: Boolean = false,
19 | val storeName: String = "",
20 | )
21 |
22 | internal data class ApkDownloadState(
23 | val shouldStartAPKDownload: Boolean = false,
24 | val downloadUrl: String = "",
25 | val shouldShowUpdateInProgress: Boolean = false,
26 | val shouldInstallApk: Boolean = false,
27 | val apk: File = File(""),
28 | )
29 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/UpdaterDialogData.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.models
2 |
3 | import android.graphics.Typeface
4 | import com.pouyaheydari.appupdater.core.model.Theme
5 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
6 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
7 |
8 | /**
9 | * This model is used to pass data to config the [com.pouyaheydari.appupdater.compose.ui.AndroidAppUpdater]
10 | */
11 | data class UpdaterDialogData(
12 | val dialogTitle: String = "",
13 | val dialogDescription: String = "",
14 | val dividerText: String = "",
15 | val storeList: List = listOf(),
16 | val directDownloadList: List = listOf(),
17 | val onDismissRequested: () -> Unit = {},
18 | val errorWhileOpeningStoreCallback: (String) -> Unit = {},
19 | val typeface: Typeface? = null,
20 | val theme: Theme = Theme.SYSTEM_DEFAULT,
21 | )
22 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/UpdaterDialogUIData.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.models
2 |
3 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
4 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
5 |
6 | internal data class UpdaterDialogUIData(
7 | val dialogHeader: DialogHeaderModel = DialogHeaderModel(),
8 | val dividerText: String = "",
9 | val directDownloadList: List = emptyList(),
10 | val storeList: List = emptyList(),
11 | val shouldShowDividers: Boolean = false,
12 | val onDismissRequested: () -> Unit = {},
13 | val onStoreClickListener: (StoreListItem) -> Unit = {},
14 | val onDirectLinkClickListener: (DirectDownloadListItem) -> Unit = {},
15 | )
16 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/models/UpdaterViewModelData.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.models
2 |
3 | import com.pouyaheydari.appupdater.core.model.Theme
4 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
5 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
6 |
7 | internal data class UpdaterViewModelData(
8 | val dialogTitle: String = "",
9 | val dialogDescription: String = "",
10 | val dividerText: String = "",
11 | val storeList: List = listOf(),
12 | val directDownloadList: List = listOf(),
13 | val onDismissRequested: () -> Unit = {},
14 | val theme: Theme = Theme.SYSTEM_DEFAULT,
15 | )
16 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Grey60 = Color(0xFF4D4D4D)
6 | val White80 = Color(0xFFEDEDED)
7 | val Blue = Color(0xFF2EA3CC)
8 | val Black7 = Color(0x11000000)
9 | val White10 = Color(0x1AFFFFFF)
10 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.darkColorScheme
6 | import androidx.compose.material3.lightColorScheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.graphics.Color
9 |
10 | private val DarkColorScheme = darkColorScheme(
11 | onSurface = White80,
12 | surface = Color.Black,
13 | background = White10,
14 | )
15 |
16 | private val LightColorScheme = lightColorScheme(
17 | onSurface = Grey60,
18 | surface = Color.White,
19 | background = Black7,
20 | )
21 |
22 | @Composable
23 | fun AndroidAppUpdaterTheme(
24 | darkTheme: Boolean = isSystemInDarkTheme(),
25 | content: @Composable () -> Unit,
26 | ) {
27 | val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
28 | MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
29 | }
30 |
--------------------------------------------------------------------------------
/compose/src/main/java/com/pouyaheydari/appupdater/compose/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontWeight
6 | import androidx.compose.ui.unit.sp
7 |
8 | val Typography = Typography(
9 | titleLarge = TextStyle(
10 | fontWeight = FontWeight.Normal,
11 | fontSize = 18.sp,
12 | lineHeight = 24.sp,
13 | letterSpacing = 0.5.sp,
14 | ),
15 | bodyMedium = TextStyle(
16 | fontWeight = FontWeight.Normal,
17 | fontSize = 14.sp,
18 | ),
19 | bodyLarge = TextStyle(
20 | fontWeight = FontWeight.Normal,
21 | fontSize = 16.sp,
22 | ),
23 | )
24 |
--------------------------------------------------------------------------------
/compose/src/test/java/com/pouyaheydari/appupdater/compose/data/mapper/UpdaterViewModelDataMapperTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.data.mapper
2 |
3 | import com.pouyaheydari.appupdater.compose.ui.models.UpdaterDialogData
4 | import com.pouyaheydari.appupdater.core.model.Theme
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 |
8 | class UpdaterViewModelDataMapperTest {
9 |
10 | @Test
11 | fun `map should correctly map UpdaterDialogData to UpdaterViewModelData`() {
12 | // Given
13 | val dialogData = UpdaterDialogData(
14 | dialogTitle = "Title",
15 | dialogDescription = "Description",
16 | dividerText = "Divider",
17 | storeList = listOf(),
18 | directDownloadList = listOf(),
19 | onDismissRequested = {},
20 | theme = Theme.SYSTEM_DEFAULT
21 | )
22 |
23 | // When
24 | val viewModelData = UpdaterViewModelDataMapper.map(dialogData)
25 |
26 | // Then
27 | assertEquals(dialogData.dialogTitle, viewModelData.dialogTitle)
28 | assertEquals(dialogData.dialogDescription, viewModelData.dialogDescription)
29 | assertEquals(dialogData.dividerText, viewModelData.dividerText)
30 | assertEquals(dialogData.storeList, viewModelData.storeList)
31 | assertEquals(dialogData.directDownloadList, viewModelData.directDownloadList)
32 | assertEquals(dialogData.onDismissRequested, viewModelData.onDismissRequested)
33 | assertEquals(dialogData.theme, viewModelData.theme)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/compose/src/test/java/com/pouyaheydari/appupdater/compose/ui/AndroidAppUpdaterViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui
2 |
3 | import app.cash.turbine.test
4 | import com.pouyaheydari.appupdater.compose.ui.models.DialogScreenIntents
5 | import com.pouyaheydari.appupdater.compose.ui.models.UpdaterViewModelData
6 | import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
7 | import com.pouyaheydari.appupdater.directdownload.domain.DownloadState
8 | import com.pouyaheydari.appupdater.directdownload.domain.GetDownloadStateUseCase
9 | import com.pouyaheydari.appupdater.directdownload.domain.SetDownloadStateUseCase
10 | import com.pouyaheydari.appupdater.store.domain.StoreListItem
11 | import com.pouyaheydari.appupdater.store.domain.stores.AppStore
12 | import kotlinx.coroutines.flow.flowOf
13 | import kotlinx.coroutines.test.runTest
14 | import org.junit.Assert.assertEquals
15 | import org.junit.Assert.assertTrue
16 | import org.junit.Before
17 | import org.junit.Test
18 | import org.mockito.Mockito.mock
19 | import org.mockito.kotlin.verify
20 | import org.mockito.kotlin.whenever
21 |
22 | class AndroidAppUpdaterViewModelTest {
23 |
24 | private val getDownloadStateUseCase: GetDownloadStateUseCase = mock()
25 | private val setDownloadStateUseCase: SetDownloadStateUseCase = mock()
26 | private lateinit var viewModel: AndroidAppUpdaterViewModel
27 |
28 | @Before
29 | fun setup() {
30 | val viewModelData = UpdaterViewModelData()
31 | viewModel = AndroidAppUpdaterViewModel(viewModelData, getDownloadStateUseCase, setDownloadStateUseCase)
32 | }
33 |
34 | @Test
35 | fun `handleIntent OnStoreClicked updates UI state correctly`() = runTest {
36 | val store: AppStore = mock()
37 | val storeItem = StoreListItem(store)
38 |
39 | viewModel.handleIntent(DialogScreenIntents.OnStoreClicked(storeItem))
40 |
41 | viewModel.uiState.test {
42 | assertEquals(store, awaitItem().selectedStore)
43 | }
44 | }
45 |
46 | @Test
47 | fun `handleIntent OnDirectLinkClicked updates UI state correctly`() = runTest {
48 | val url = "https://example.com/app.apk"
49 | val title = "Direct Link"
50 | val downloadItem = DirectDownloadListItem(title = title, url = url)
51 |
52 | viewModel.handleIntent(DialogScreenIntents.OnDirectLinkClicked(downloadItem))
53 |
54 | viewModel.uiState.test {
55 | val item = awaitItem()
56 | assertEquals(url, item.downloadState.downloadUrl)
57 | assertTrue(item.downloadState.shouldStartAPKDownload)
58 | }
59 | }
60 |
61 | @Test
62 | fun `handleIntent OnApkDownloadStarted triggers setDownloadStateUseCase`() = runTest {
63 | whenever(getDownloadStateUseCase()).thenReturn(flowOf(DownloadState.Downloading))
64 |
65 | viewModel.handleIntent(DialogScreenIntents.OnApkDownloadStarted)
66 |
67 | verify(setDownloadStateUseCase).invoke(DownloadState.Downloading)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/compose/src/test/java/com/pouyaheydari/appupdater/compose/ui/components/DialogHeaderComponentTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.junit4.createComposeRule
5 | import androidx.compose.ui.test.onNodeWithTag
6 | import androidx.compose.ui.test.onNodeWithText
7 | import com.pouyaheydari.appupdater.compose.ui.models.DialogHeaderModel
8 | import org.junit.Rule
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.robolectric.RobolectricTestRunner
12 | import com.pouyaheydari.appupdater.store.R as storeR
13 |
14 | @RunWith(RobolectricTestRunner::class)
15 | internal class DialogHeaderComponentTest {
16 | @get:Rule
17 | val composeTestRule = createComposeRule()
18 |
19 | @Test
20 | fun test_whenComponentIsCalled_thenCorrectUiIsBeingDisplayed() {
21 | val dialogTitle = "Title"
22 | val dialogDescription = "Description"
23 | val dialogIcon = storeR.drawable.appupdater_ic_google_play
24 | composeTestRule.setContent {
25 | DialogHeaderComponent(
26 | content = DialogHeaderModel(
27 | dialogTitle = dialogTitle,
28 | dialogDescription = dialogDescription,
29 | dialogIcon = dialogIcon,
30 | ),
31 | )
32 | }
33 |
34 | composeTestRule.onNodeWithText(dialogTitle).assertIsDisplayed()
35 | composeTestRule.onNodeWithText(dialogDescription).assertIsDisplayed()
36 | composeTestRule.onNodeWithTag(dialogIcon.toString()).assertIsDisplayed()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/compose/src/test/java/com/pouyaheydari/appupdater/compose/ui/components/DirectDownloadLinkComponentTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.junit4.createComposeRule
5 | import androidx.compose.ui.test.onNodeWithText
6 | import androidx.compose.ui.test.performClick
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.mockito.Mockito.mock
11 | import org.mockito.kotlin.verify
12 | import org.robolectric.RobolectricTestRunner
13 |
14 | @RunWith(RobolectricTestRunner::class)
15 | internal class DirectDownloadLinkComponentTest {
16 | @get:Rule
17 | val composeTestRule = createComposeRule()
18 |
19 | @Test
20 | fun test_whenComponentIsCalled_thenCorrectTextIsBeingDisplayed() {
21 | val title = "Text"
22 |
23 | composeTestRule.setContent {
24 | DirectDownloadLinkComponent(title = title)
25 | }
26 |
27 | composeTestRule.onNodeWithText(title).assertIsDisplayed()
28 | }
29 |
30 | @Test
31 | fun test_whenComponentIsCalled_andUserClickOnText_thenOnClickListenerIsCalled() {
32 | val title = "Text"
33 | val clickListener: () -> Unit = mock()
34 |
35 | composeTestRule.setContent {
36 | DirectDownloadLinkComponent(title = title, onClickListener = clickListener)
37 | }
38 | composeTestRule.onNodeWithText(title).performClick()
39 |
40 | verify(clickListener).invoke()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/compose/src/test/java/com/pouyaheydari/appupdater/compose/ui/components/DividerComponentTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.assertTextEquals
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithText
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.robolectric.RobolectricTestRunner
11 |
12 | @RunWith(RobolectricTestRunner::class)
13 | internal class DividerComponentTest {
14 | @get:Rule
15 | val composeTestRule = createComposeRule()
16 |
17 | @Test
18 | fun test_whenComponentIsCalled_thenCorrectTextIsDisplayed() {
19 | val dividerText = "Text"
20 |
21 | composeTestRule.setContent {
22 | DividerComponent(dividerText = dividerText)
23 | }
24 |
25 | composeTestRule.onNodeWithText(dividerText)
26 | .assertIsDisplayed()
27 | .assertTextEquals(dividerText)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/compose/src/test/java/com/pouyaheydari/appupdater/compose/ui/components/SquareStoreItemComponentTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.assertTextEquals
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.onNodeWithText
8 | import androidx.compose.ui.test.onRoot
9 | import androidx.compose.ui.test.performClick
10 | import org.junit.Rule
11 | import org.junit.Test
12 | import org.junit.runner.RunWith
13 | import org.mockito.Mockito
14 | import org.mockito.kotlin.verify
15 | import org.robolectric.RobolectricTestRunner
16 | import com.pouyaheydari.appupdater.store.R as storeR
17 |
18 | @RunWith(RobolectricTestRunner::class)
19 | internal class SquareStoreItemComponentTest {
20 | @get:Rule
21 | val composeTestRule = createComposeRule()
22 |
23 | @Test
24 | fun test_whenComponentIsCalled_thenCorrectTextIsBeingDisplayed() {
25 | val storeTitle = "Title"
26 |
27 | composeTestRule.setContent {
28 | SquareStoreItemComponent(title = storeTitle)
29 | }
30 |
31 | composeTestRule.onNodeWithText(storeTitle)
32 | .assertIsDisplayed()
33 | .assertTextEquals(storeTitle)
34 | }
35 |
36 | @Test
37 | fun test_whenComponentIsCalled_thenCorrectIconIsBeingDisplayed() {
38 | val storeIcon = storeR.drawable.appupdater_ic_google_play
39 |
40 | composeTestRule.setContent {
41 | SquareStoreItemComponent(icon = storeIcon)
42 | }
43 |
44 | composeTestRule.onNodeWithTag(storeIcon.toString(), useUnmergedTree = true).assertIsDisplayed()
45 | }
46 |
47 | @Test
48 | fun test_whenComponentIsCalled_andUserClickOnText_thenOnClickListenerIsCalled() {
49 | val clickListener: () -> Unit = Mockito.mock()
50 |
51 | composeTestRule.setContent {
52 | SquareStoreItemComponent(onClickListener = clickListener)
53 | }
54 | composeTestRule.onRoot().performClick()
55 |
56 | verify(clickListener).invoke()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/compose/src/test/java/com/pouyaheydari/appupdater/compose/ui/components/UpdateInProgressDialogComponentTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.compose.ui.components
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.junit4.createComposeRule
5 | import androidx.compose.ui.test.onNodeWithTag
6 | import androidx.compose.ui.test.onNodeWithText
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.robolectric.RobolectricTestRunner
11 |
12 | @RunWith(RobolectricTestRunner::class)
13 | internal class UpdateInProgressDialogComponentTest {
14 | @get:Rule
15 | val composeTestRule = createComposeRule()
16 |
17 | @Test
18 | fun test_whenComponentIsCalled_andTheDialogMustBeVisible_thenCorrectNodesAreBeingShown() {
19 | val dialogTitle = "Title"
20 | val dialogDescription = "Description"
21 | composeTestRule.setContent {
22 | UpdateInProgressDialogComponent(
23 | isUpdateInProgress = true,
24 | dialogTitle = dialogTitle,
25 | dialogDescription = dialogDescription,
26 | )
27 | }
28 |
29 | composeTestRule.onNodeWithText(dialogTitle).assertIsDisplayed()
30 | composeTestRule.onNodeWithText(dialogDescription).assertIsDisplayed()
31 | composeTestRule.onNodeWithTag(DIALOG_TEST_TAG).assertIsDisplayed()
32 | }
33 |
34 | @Test
35 | fun test_whenComponentIsCalled_andTheDialogMustBeInvisible_thenNoNodesAreShown() {
36 | val dialogTitle = "Title"
37 | val dialogDescription = "Description"
38 | composeTestRule.setContent {
39 | UpdateInProgressDialogComponent(
40 | isUpdateInProgress = false,
41 | dialogTitle = dialogTitle,
42 | dialogDescription = dialogDescription,
43 | )
44 | }
45 |
46 | composeTestRule.onNodeWithText(dialogTitle).assertDoesNotExist()
47 | composeTestRule.onNodeWithText(dialogDescription).assertDoesNotExist()
48 | composeTestRule.onNodeWithTag(DIALOG_TEST_TAG).assertDoesNotExist()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlin.jvm)
3 | alias(libs.plugins.maven.publish)
4 | }
5 |
--------------------------------------------------------------------------------
/core/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=core
2 |
--------------------------------------------------------------------------------
/core/src/main/java/com/pouyaheydari/appupdater/core/model/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.core.model
2 |
3 | /**
4 | * Specifies the theme of dialog
5 | */
6 | enum class Theme { LIGHT, DARK, SYSTEM_DEFAULT }
7 |
--------------------------------------------------------------------------------
/core/src/main/java/com/pouyaheydari/appupdater/core/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.core.utils
2 |
3 | const val ANDROID_APP_UPDATER_DEBUG_TAG = "AndroidAppUpdater"
4 | const val APK_NAME = "NewAPK.apk"
5 |
--------------------------------------------------------------------------------
/directdownload/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/directdownload/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | alias(libs.plugins.maven.publish)
5 | }
6 |
7 | android {
8 | compileSdk = libs.versions.compileSdkVersion.get().toInt()
9 | defaultConfig {
10 | minSdk = libs.versions.minSdkVersion.get().toInt()
11 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
12 | }
13 |
14 | compileOptions {
15 | sourceCompatibility = JavaVersion.VERSION_17
16 | targetCompatibility = JavaVersion.VERSION_17
17 | }
18 | kotlinOptions {
19 | jvmTarget = JavaVersion.VERSION_17.toString()
20 | }
21 | namespace = "com.pouyaheydari.appupdater.directdownload"
22 | }
23 |
24 | dependencies {
25 | api(projects.core)
26 |
27 | implementation(libs.androidx.appcompat)
28 | implementation(libs.kotlinx.coroutines)
29 |
30 | // testing
31 | testImplementation(libs.junit4)
32 | testImplementation(libs.mockito)
33 | testImplementation(libs.mockito.kotlin)
34 | testImplementation (libs.kotlinx.coroutines.test)
35 | testImplementation (libs.roboelectric)
36 |
37 | androidTestImplementation(libs.androidx.test.junit)
38 | androidTestImplementation(libs.androidx.test.rules)
39 | androidTestImplementation(libs.androidx.test.ui.espresso.core)
40 | androidTestImplementation(libs.androidx.test.ui.espresso.intents)
41 | }
42 |
--------------------------------------------------------------------------------
/directdownload/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=directdownload
2 |
--------------------------------------------------------------------------------
/directdownload/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
11 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/data/DirectDownloadListItem.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.data
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 |
6 | data class DirectDownloadListItem(var title: String = "", var url: String = "") : Parcelable {
7 | private constructor(parcel: Parcel) : this(
8 | parcel.readString().orEmpty(),
9 | parcel.readString().orEmpty(),
10 | )
11 |
12 | override fun describeContents(): Int = 0
13 |
14 | override fun writeToParcel(dest: Parcel, flags: Int) {
15 | dest.writeString(title)
16 | dest.writeString(url)
17 | }
18 |
19 | companion object CREATOR : Parcelable.Creator {
20 | override fun createFromParcel(parcel: Parcel): DirectDownloadListItem {
21 | return DirectDownloadListItem(parcel)
22 | }
23 |
24 | override fun newArray(size: Int): Array {
25 | return arrayOfNulls(size)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/data/UpdateInProgressRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.data
2 |
3 | import com.pouyaheydari.appupdater.directdownload.domain.DownloadState
4 | import com.pouyaheydari.appupdater.directdownload.domain.UpdateInProgressRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.MutableSharedFlow
7 | import kotlinx.coroutines.flow.asSharedFlow
8 | import java.util.concurrent.atomic.AtomicLong
9 |
10 | private const val NO_REQUEST_ID = -10L
11 |
12 | object UpdateInProgressRepositoryImpl : UpdateInProgressRepository {
13 | private var requestId: AtomicLong = AtomicLong(NO_REQUEST_ID)
14 | private val downloadState = MutableSharedFlow(replay = 1)
15 |
16 | override fun getRequestId(): Long = requestId.get()
17 |
18 | override fun setRequestId(id: Long) {
19 | requestId.set(id)
20 | }
21 |
22 | override fun getDownloadState(): Flow = downloadState.asSharedFlow()
23 |
24 | override suspend fun updateApkDownloadState(state: DownloadState) {
25 | downloadState.emit(state)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/DownloadState.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | import java.io.File
4 |
5 | sealed interface DownloadState {
6 | data object Downloading : DownloadState
7 | data class Downloaded(val apk: File) : DownloadState
8 | }
9 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/GetDownloadStateUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | class GetDownloadStateUseCase(private val repository: UpdateInProgressRepository) {
6 | operator fun invoke(): Flow = repository.getDownloadState()
7 | }
8 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/GetRequestIdUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | internal class GetRequestIdUseCase(private val repository: UpdateInProgressRepository) {
4 | operator fun invoke(): Long = repository.getRequestId()
5 | }
6 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/SetDownloadStateUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | class SetDownloadStateUseCase(private val repository: UpdateInProgressRepository) {
4 | suspend operator fun invoke(downloadState: DownloadState) {
5 | repository.updateApkDownloadState(downloadState)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/SetRequestIdUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | class SetRequestIdUseCase(private val repository: UpdateInProgressRepository) {
4 | operator fun invoke(requestId: Long) {
5 | repository.setRequestId(requestId)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/domain/UpdateInProgressRepository.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface UpdateInProgressRepository {
6 | fun getRequestId(): Long
7 | fun setRequestId(id: Long)
8 | fun getDownloadState(): Flow
9 | suspend fun updateApkDownloadState(state: DownloadState)
10 | }
11 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/receiver/DownloadFinishedReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.receiver
2 |
3 | import android.app.DownloadManager
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import com.pouyaheydari.appupdater.directdownload.data.UpdateInProgressRepositoryImpl
8 | import com.pouyaheydari.appupdater.directdownload.domain.DownloadState
9 | import com.pouyaheydari.appupdater.directdownload.domain.GetRequestIdUseCase
10 | import com.pouyaheydari.appupdater.directdownload.domain.SetDownloadStateUseCase
11 | import com.pouyaheydari.appupdater.directdownload.utils.donwloadapk.APKFileProviderImpl
12 | import kotlinx.coroutines.CoroutineScope
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.launch
15 |
16 | internal class DownloadFinishedReceiver : BroadcastReceiver() {
17 | private val coroutineScope = CoroutineScope(Dispatchers.Main)
18 | private val getRequestIdUseCase by lazy { GetRequestIdUseCase(UpdateInProgressRepositoryImpl) }
19 | private val setDownloadStateUseCase by lazy { SetDownloadStateUseCase(UpdateInProgressRepositoryImpl) }
20 | private val getFilePathProvider by lazy { APKFileProviderImpl() }
21 |
22 | override fun onReceive(context: Context, intent: Intent?) {
23 | if (intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
24 | val referenceId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
25 |
26 | if (referenceId == getRequestIdUseCase()) {
27 | notifyApkDownloaded(context)
28 | }
29 | }
30 | }
31 |
32 | private fun notifyApkDownloaded(context: Context) {
33 | coroutineScope.launch {
34 | val apk = getFilePathProvider.getFile(context = context)
35 | setDownloadStateUseCase(DownloadState.Downloaded(apk))
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKDownloadManager.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk
2 |
3 | import android.app.DownloadManager
4 | import android.content.Context
5 | import android.util.Log
6 | import androidx.core.net.toUri
7 | import com.pouyaheydari.appupdater.core.utils.ANDROID_APP_UPDATER_DEBUG_TAG
8 | import com.pouyaheydari.appupdater.directdownload.data.UpdateInProgressRepositoryImpl
9 | import com.pouyaheydari.appupdater.directdownload.domain.SetRequestIdUseCase
10 |
11 | class APKDownloadManager(
12 | private val downloadManagerRequestCreator: DownloadManagerRequestCreator = DownloadManagerRequestCreator(),
13 | private val setRequestIdUseCase: SetRequestIdUseCase = SetRequestIdUseCase(UpdateInProgressRepositoryImpl),
14 | private val apkFileProvider: APKFileProvider = APKFileProviderImpl(),
15 | ) {
16 | fun deleteExistingAPKAndDownloadNewAPK(
17 | downloadManager: DownloadManager,
18 | url: String,
19 | context: Context,
20 | notificationTitle: String,
21 | notificationDescription: String,
22 | ) {
23 | safeDeleteIfPreviousAPKExists(context)
24 | downloadAPK(downloadManager, url, context, notificationTitle, notificationDescription)
25 | }
26 |
27 | private fun downloadAPK(
28 | downloadManager: DownloadManager,
29 | url: String,
30 | context: Context,
31 | notificationTitle: String,
32 | notificationDescription: String,
33 | ) {
34 | val downloadManagerRequest = downloadManagerRequestCreator.create(
35 | uri = url.toUri(),
36 | context = context,
37 | notificationTitle = notificationTitle,
38 | notificationDescription = notificationDescription
39 | )
40 | setRequestIdUseCase(downloadManager.enqueue(downloadManagerRequest))
41 | }
42 |
43 | private fun safeDeleteIfPreviousAPKExists(context: Context) {
44 | val file = apkFileProvider.getFile(context)
45 | try {
46 | if (file.exists()) {
47 | file.delete()
48 | }
49 | } catch (exception: SecurityException) {
50 | Log.e(ANDROID_APP_UPDATER_DEBUG_TAG, "Error while deleting existing APK file: ${exception.message}")
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProvider.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk
2 |
3 | import android.content.Context
4 | import java.io.File
5 |
6 | interface APKFileProvider {
7 | fun getFile(context: Context): File
8 | }
9 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk
2 |
3 | import android.content.Context
4 | import android.os.Environment
5 | import com.pouyaheydari.appupdater.core.utils.APK_NAME
6 | import java.io.File
7 |
8 | class APKFileProviderImpl : APKFileProvider {
9 | override fun getFile(context: Context) =
10 | File("${context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)}/$APK_NAME")
11 | }
12 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadAPKHelper.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk
2 |
3 | import android.app.Activity
4 | import android.app.DownloadManager
5 | import com.pouyaheydari.appupdater.directdownload.utils.permission.DownloadAPKPermission
6 | import com.pouyaheydari.appupdater.directdownload.utils.permission.DownloadAPKPermissionFactory
7 |
8 | fun checkPermissionsAndDownloadApk(
9 | url: String,
10 | activity: Activity,
11 | androidSdkVersion: Int,
12 | notificationTitle: String,
13 | notificationDescription: String,
14 | downloadManager: DownloadManager,
15 | downloadAPKPermission: DownloadAPKPermission = DownloadAPKPermissionFactory().getDownloadAPKPermissionHandler(androidSdkVersion),
16 | apkDownloadManager: APKDownloadManager = APKDownloadManager(),
17 | onDownloadingApkStarted: () -> Unit
18 | ) {
19 | if (downloadAPKPermission.resolvePermissions(activity)) {
20 | apkDownloadManager.deleteExistingAPKAndDownloadNewAPK(
21 | url = url,
22 | context = activity,
23 | notificationTitle = notificationTitle,
24 | notificationDescription = notificationDescription,
25 | downloadManager = downloadManager
26 | )
27 | onDownloadingApkStarted()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadManagerRequestCreator.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk
2 |
3 | import android.app.DownloadManager
4 | import android.app.DownloadManager.Request.NETWORK_MOBILE
5 | import android.app.DownloadManager.Request.NETWORK_WIFI
6 | import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
7 | import android.content.Context
8 | import android.net.Uri
9 | import android.os.Environment
10 | import android.util.Log
11 | import com.pouyaheydari.appupdater.core.utils.ANDROID_APP_UPDATER_DEBUG_TAG
12 | import com.pouyaheydari.appupdater.core.utils.APK_NAME
13 |
14 | class DownloadManagerRequestCreator {
15 |
16 | fun create(
17 | uri: Uri,
18 | context: Context,
19 | notificationTitle: String,
20 | notificationDescription: String,
21 | downloadManagerRequest: DownloadManager.Request = DownloadManager.Request(uri)
22 | ): DownloadManager.Request = downloadManagerRequest.apply {
23 | setTitle(notificationTitle)
24 | setDescription(notificationDescription)
25 | setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
26 | setAllowedNetworkTypes(NETWORK_WIFI or NETWORK_MOBILE)
27 | try {
28 | setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, APK_NAME)
29 | } catch (exception: IllegalStateException) {
30 | Log.e(ANDROID_APP_UPDATER_DEBUG_TAG, "Error while setting download destination for download manager request: ${exception.message}")
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/APKIntentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Build
6 | import java.io.File
7 |
8 | class APKIntentFactory(private val fileUriProvider: FileUriProvider = FileUriProvider()) {
9 |
10 | fun getInstallAPKIntent(context: Context, apk: File, androidSdkVersion: Int): Intent {
11 | val uri = fileUriProvider.getFileUri(context, apk, androidSdkVersion)
12 | return when {
13 | androidSdkVersion >= Build.VERSION_CODES.P -> InstallAPKIntentForPAndAbove().getIntent(uri)
14 | androidSdkVersion >= Build.VERSION_CODES.N -> InstallAPKIntentForNToO().getIntent(uri)
15 | else -> InstallAPKIntentForM().getIntent(uri)
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/FileUriProvider.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.os.Build
6 | import androidx.core.content.FileProvider
7 | import java.io.File
8 |
9 | class FileUriProvider {
10 |
11 | fun getFileUri(context: Context, apk: File, androidSdkVersion: Int): Uri = when (androidSdkVersion) {
12 | Build.VERSION_CODES.M -> Uri.fromFile(apk)
13 | else -> FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apk)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallAPKIntent.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 |
6 | internal interface InstallAPKIntent {
7 | fun getIntent(fileUri: Uri): Intent
8 | }
9 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallAPKIntentForM.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 |
6 | private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
7 |
8 | internal class InstallAPKIntentForM : InstallAPKIntent {
9 | override fun getIntent(fileUri: Uri) =
10 | Intent(Intent.ACTION_VIEW, fileUri).apply {
11 | setDataAndType(fileUri, APK_MIME_TYPE)
12 | flags = Intent.FLAG_ACTIVITY_NEW_TASK
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallAPKIntentForNToO.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 |
6 | @Suppress("DEPRECATION")
7 | internal class InstallAPKIntentForNToO : InstallAPKIntent {
8 | override fun getIntent(fileUri: Uri) =
9 | Intent(Intent.ACTION_INSTALL_PACKAGE, fileUri).apply {
10 | flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallAPKIntentForPAndAbove.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 |
6 | internal class InstallAPKIntentForPAndAbove : InstallAPKIntent {
7 | override fun getIntent(fileUri: Uri) =
8 | Intent(Intent.ACTION_VIEW, fileUri).apply {
9 | flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallApkUtil.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.ActivityNotFoundException
4 | import android.content.Context
5 | import android.util.Log
6 | import com.pouyaheydari.appupdater.core.utils.ANDROID_APP_UPDATER_DEBUG_TAG
7 | import java.io.File
8 |
9 | fun installAPK(context: Context, apk: File, androidSdkVersion: Int, apkIntentFactory: APKIntentFactory = APKIntentFactory()) {
10 | if (!apk.exists()) {
11 | Log.e(ANDROID_APP_UPDATER_DEBUG_TAG, "APK file not found")
12 | return
13 | }
14 |
15 | val intent = apkIntentFactory.getInstallAPKIntent(context, apk, androidSdkVersion)
16 | try {
17 | context.startActivity(intent)
18 | } catch (e: ActivityNotFoundException) {
19 | Log.e(ANDROID_APP_UPDATER_DEBUG_TAG, "Activity not found for installing APK: ${e.message.orEmpty()}")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermission.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.permission
2 |
3 | import android.app.Activity
4 |
5 | interface DownloadAPKPermission {
6 | fun resolvePermissions(activity: Activity): Boolean
7 | }
8 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactory.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.permission
2 |
3 | class DownloadAPKPermissionFactory {
4 |
5 | fun getDownloadAPKPermissionHandler(androidSdkVersion: Int): DownloadAPKPermission = when {
6 | androidSdkVersion >= android.os.Build.VERSION_CODES.P -> DownloadAPKPermissionForPAndAbove()
7 | else -> DownloadAPKPermissionForOAndBellow()
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBellow.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.permission
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 | import androidx.annotation.RequiresApi
8 | import androidx.core.app.ActivityCompat
9 | import androidx.core.content.ContextCompat
10 |
11 | private const val PERMISSION_REQUEST_CODE = 2000
12 |
13 | internal class DownloadAPKPermissionForOAndBellow : DownloadAPKPermission {
14 |
15 | @RequiresApi(Build.VERSION_CODES.O)
16 | override fun resolvePermissions(activity: Activity): Boolean {
17 | return if (isExternalStoragePermissionGranted(activity)) {
18 | true
19 | } else {
20 | getWriteToStoragePermission(activity)
21 | false
22 | }
23 | }
24 |
25 | private fun isExternalStoragePermissionGranted(activity: Activity) =
26 | ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
27 |
28 | private fun getWriteToStoragePermission(activity: Activity) {
29 | ActivityCompat.requestPermissions(
30 | activity,
31 | arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
32 | PERMISSION_REQUEST_CODE,
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/directdownload/src/main/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForPAndAbove.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.permission
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Build
7 | import android.provider.Settings
8 | import androidx.annotation.RequiresApi
9 | import androidx.core.net.toUri
10 | import org.jetbrains.annotations.VisibleForTesting
11 | import java.util.Locale
12 |
13 |
14 | internal class DownloadAPKPermissionForPAndAbove : DownloadAPKPermission {
15 |
16 | @RequiresApi(Build.VERSION_CODES.O)
17 | override fun resolvePermissions(activity: Activity): Boolean {
18 | return if (!activity.packageManager.canRequestPackageInstalls()) {
19 | requestInstallFromUnknownSourcePermission(activity)
20 | false
21 | } else {
22 | true
23 | }
24 | }
25 |
26 | @RequiresApi(Build.VERSION_CODES.O)
27 | private fun requestInstallFromUnknownSourcePermission(context: Context) {
28 | val intent = getInstallFromUnknownSourceIntent(context)
29 | context.startActivity(intent)
30 | }
31 |
32 | @VisibleForTesting
33 | @RequiresApi(Build.VERSION_CODES.O)
34 | internal fun getInstallFromUnknownSourceIntent(context: Context): Intent {
35 | val uri = String.format(Locale.ENGLISH, "package:%s", context.packageName).toUri()
36 | val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, uri).apply {
37 | flags = Intent.FLAG_ACTIVITY_NEW_TASK
38 | }
39 | return intent
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/directdownload/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | App Updater
3 | Please wait
4 | Downloading new version…
5 | Downloading…
6 | Downloading new version
7 | Please install %s
8 | or
9 | Download from store
10 | Couldn\'t find downloaded file.
11 |
12 |
--------------------------------------------------------------------------------
/directdownload/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/data/UpdateInProgressRepositoryImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.data
2 |
3 | import com.pouyaheydari.appupdater.directdownload.domain.DownloadState
4 | import kotlinx.coroutines.flow.first
5 | import kotlinx.coroutines.test.runTest
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Before
8 | import org.junit.Test
9 | import java.io.File
10 |
11 | class UpdateInProgressRepositoryImplTest {
12 |
13 | private lateinit var repository: UpdateInProgressRepositoryImpl
14 |
15 | @Before
16 | fun setUp() {
17 | repository = UpdateInProgressRepositoryImpl
18 | }
19 |
20 | @Test
21 | fun `When request id is set by setRequestId, then correct request id will be returned by getRequestId`() {
22 | val newRequestId = 12345L
23 |
24 | repository.setRequestId(newRequestId)
25 |
26 | val requestId = repository.getRequestId()
27 | assertEquals(newRequestId, requestId)
28 | }
29 |
30 | @Test
31 | fun `When updateApkDownloadState is called, then the correct state is emitted`() = runTest {
32 | val newState = DownloadState.Downloading
33 |
34 | repository.updateApkDownloadState(newState)
35 |
36 | val state = repository.getDownloadState().first()
37 | assertEquals(newState, state)
38 | }
39 |
40 | @Test
41 | fun `When updateApkDownloadState is called with Downloaded state, then the correct state is emitted`() = runTest {
42 | val apkFile = File("/path/to/downloaded/apk")
43 | val newState = DownloadState.Downloaded(apkFile)
44 |
45 | repository.updateApkDownloadState(newState)
46 |
47 | val state = repository.getDownloadState().first()
48 | assertEquals(newState, state)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/domain/GetDownloadStateUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | import kotlinx.coroutines.flow.first
4 | import kotlinx.coroutines.flow.flowOf
5 | import kotlinx.coroutines.test.runTest
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Test
8 | import org.mockito.kotlin.mock
9 | import org.mockito.kotlin.whenever
10 | import java.io.File
11 |
12 | class GetDownloadStateUseCaseTest {
13 |
14 | private val repository: UpdateInProgressRepository = mock()
15 | private val getDownloadStateUseCase = GetDownloadStateUseCase(repository)
16 |
17 | @Test
18 | fun `When state is downloaded, then use case returns the correct state`() = runTest {
19 | val expected = DownloadState.Downloaded(File(""))
20 | whenever(repository.getDownloadState()).thenReturn(flowOf(expected))
21 |
22 | val downloadState = getDownloadStateUseCase().first()
23 | assertEquals(expected, downloadState)
24 | }
25 |
26 | @Test
27 | fun `When state is downloading, then use case returns the correct state`() = runTest {
28 | whenever(repository.getDownloadState()).thenReturn(flowOf(DownloadState.Downloading))
29 |
30 | val downloadState = getDownloadStateUseCase().first()
31 | assertEquals(DownloadState.Downloading, downloadState)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/domain/GetRequestIdUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 | import org.mockito.kotlin.mock
6 | import org.mockito.kotlin.whenever
7 |
8 | class GetRequestIdUseCaseTest {
9 |
10 | private val repository: UpdateInProgressRepository = mock()
11 | private val getRequestIdUseCase = GetRequestIdUseCase(repository)
12 |
13 | @Test
14 | fun `When repository returns request id, then use case returns the same id`() {
15 | val requestId = 12345L
16 | whenever(repository.getRequestId()).thenReturn(requestId)
17 |
18 | val result = getRequestIdUseCase()
19 | assertEquals(requestId, result)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/domain/SetDownloadStateUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | import kotlinx.coroutines.test.runTest
4 | import org.junit.Test
5 | import org.mockito.kotlin.mock
6 | import org.mockito.kotlin.verify
7 |
8 | class SetDownloadStateUseCaseTest {
9 |
10 | private val repository: UpdateInProgressRepository = mock()
11 | private val setDownloadStateUseCase = SetDownloadStateUseCase(repository)
12 |
13 | @Test
14 | fun `When invoked, then repository with the correct value is called`() = runTest {
15 | val downloadState = DownloadState.Downloading
16 |
17 | setDownloadStateUseCase(downloadState)
18 |
19 | verify(repository).updateApkDownloadState(downloadState)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/domain/SetRequestIdUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.domain
2 |
3 | import org.junit.Test
4 | import org.mockito.kotlin.mock
5 | import org.mockito.kotlin.verify
6 |
7 | class SetRequestIdUseCaseTest {
8 |
9 | private val repository: UpdateInProgressRepository = mock()
10 | private val setRequestIdUseCase = SetRequestIdUseCase(repository)
11 |
12 | @Test
13 | fun `When invoked with request id, then repository with the correct request id is called`() {
14 | val requestId = 12345L
15 |
16 | setRequestIdUseCase(requestId)
17 |
18 | verify(repository).setRequestId(requestId)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/APKFileProviderImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk
2 |
3 | import android.content.Context
4 | import android.os.Environment
5 | import com.pouyaheydari.appupdater.core.utils.APK_NAME
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Before
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.mockito.junit.MockitoJUnitRunner
11 | import org.mockito.kotlin.mock
12 | import org.mockito.kotlin.whenever
13 | import java.io.File
14 |
15 | @RunWith(MockitoJUnitRunner::class)
16 | class APKFileProviderImplTest {
17 |
18 | private val context: Context = mock()
19 | private lateinit var apkFileProvider: APKFileProviderImpl
20 |
21 | @Before
22 | fun setUp() {
23 | apkFileProvider = APKFileProviderImpl()
24 | }
25 |
26 | @Test
27 | fun `getFile returns correct file path`() {
28 | val mockExternalFilesDir = File("/mock/external/files/dir")
29 | whenever(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)).thenReturn(mockExternalFilesDir)
30 |
31 | val expectedFile = File("/mock/external/files/dir/$APK_NAME")
32 | val actualFile = apkFileProvider.getFile(context)
33 |
34 | assertEquals(expectedFile, actualFile)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadAPKHelperKtTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk
2 |
3 | import android.app.Activity
4 | import android.app.DownloadManager
5 | import com.pouyaheydari.appupdater.directdownload.utils.permission.DownloadAPKPermission
6 | import org.junit.Test
7 | import org.mockito.Mockito.anyString
8 | import org.mockito.Mockito.mock
9 | import org.mockito.Mockito.never
10 | import org.mockito.Mockito.verify
11 | import org.mockito.kotlin.anyOrNull
12 | import org.mockito.kotlin.whenever
13 |
14 | class DownloadAPKHelperTest {
15 |
16 | private val url = "https://example.com/app.apk"
17 | private val notificationTitle = "New Update"
18 | private val notificationDescription = "Downloading latest version"
19 | private val androidSdkVersion = 30
20 | private val activity: Activity = mock(Activity::class.java)
21 | private val downloadManager: DownloadManager = mock(DownloadManager::class.java)
22 | private val downloadAPKPermission: DownloadAPKPermission = mock(DownloadAPKPermission::class.java)
23 | private val apkDownloadManager: APKDownloadManager = mock(APKDownloadManager::class.java)
24 | private val onDownloadingApkStarted: () -> Unit = mock()
25 |
26 | @Test
27 | fun `checkPermissionsAndDownloadApk starts download when permissions are resolved`() {
28 | whenever(downloadAPKPermission.resolvePermissions(activity)).thenReturn(true)
29 |
30 | checkPermissionsAndDownloadApk(
31 | url,
32 | activity,
33 | androidSdkVersion,
34 | notificationTitle,
35 | notificationDescription,
36 | downloadManager,
37 | downloadAPKPermission,
38 | apkDownloadManager,
39 | onDownloadingApkStarted
40 | )
41 |
42 | verify(apkDownloadManager).deleteExistingAPKAndDownloadNewAPK(
43 | downloadManager,
44 | url,
45 | activity,
46 | notificationTitle,
47 | notificationDescription,
48 | )
49 | verify(onDownloadingApkStarted).invoke()
50 | }
51 |
52 | @Test
53 | fun `checkPermissionsAndDownloadApk does not start download when permissions are not resolved`() {
54 | whenever(downloadAPKPermission.resolvePermissions(activity)).thenReturn(false)
55 |
56 | checkPermissionsAndDownloadApk(
57 | url,
58 | activity,
59 | androidSdkVersion,
60 | notificationTitle,
61 | notificationDescription,
62 | downloadManager,
63 | downloadAPKPermission,
64 | apkDownloadManager,
65 | onDownloadingApkStarted
66 | )
67 |
68 | verify(apkDownloadManager, never()).deleteExistingAPKAndDownloadNewAPK(
69 | anyOrNull(),
70 | anyString(),
71 | anyOrNull(),
72 | anyString(),
73 | anyOrNull()
74 | )
75 | verify(onDownloadingApkStarted, never()).invoke()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/donwloadapk/DownloadManagerRequestCreatorTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.donwloadapk
2 |
3 | import android.app.DownloadManager
4 | import android.content.Context
5 | import android.net.Uri
6 | import android.os.Environment
7 | import androidx.core.net.toUri
8 | import com.pouyaheydari.appupdater.core.utils.APK_NAME
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.mockito.Mockito.mock
12 | import org.mockito.Mockito.verify
13 | import org.mockito.kotlin.spy
14 | import org.robolectric.RobolectricTestRunner
15 |
16 | @RunWith(RobolectricTestRunner::class)
17 | class DownloadManagerRequestCreatorTest {
18 |
19 | private val context: Context = mock()
20 | private val downloadManagerRequestCreator: DownloadManagerRequestCreator = DownloadManagerRequestCreator()
21 |
22 | @Test
23 | fun `create request with correct parameters`() {
24 | val uri: Uri = "https://example.com/app.apk".toUri()
25 | val notificationTitle = "Downloading Update"
26 | val notificationDescription = "Your update is being downloaded."
27 | val request = spy(DownloadManager.Request(uri))
28 |
29 | val createdRequest = downloadManagerRequestCreator.create(uri, context, notificationTitle, notificationDescription, request)
30 |
31 | verify(createdRequest).setTitle(notificationTitle)
32 | verify(createdRequest).setDescription(notificationDescription)
33 | verify(createdRequest).setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
34 | verify(createdRequest).setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
35 | verify(createdRequest).setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, APK_NAME)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/APKIntentFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Build
7 | import junit.framework.TestCase.assertEquals
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.mockito.Mockito.mock
11 | import org.mockito.Mockito.times
12 | import org.mockito.Mockito.verify
13 | import org.mockito.kotlin.whenever
14 | import org.robolectric.RobolectricTestRunner
15 | import java.io.File
16 |
17 | @RunWith(RobolectricTestRunner::class)
18 | class APKIntentFactoryTest {
19 | private val context: Context = mock()
20 | private val apk: File = mock()
21 | private val fileUriProvider: FileUriProvider = mock()
22 | private val uri: Uri = mock()
23 | private val apkIntentFactory = APKIntentFactory(fileUriProvider)
24 |
25 | @Test
26 | fun `test getInstallAPKIntent for Android P and above`() {
27 | whenever(fileUriProvider.getFileUri(context, apk, Build.VERSION_CODES.P)).thenReturn(uri)
28 |
29 | val intent = apkIntentFactory.getInstallAPKIntent(context, apk, Build.VERSION_CODES.P)
30 |
31 | verify(fileUriProvider, times(1)).getFileUri(context, apk, Build.VERSION_CODES.P)
32 | assertEquals(Intent.ACTION_VIEW, intent.action)
33 | assertEquals(uri, intent.data)
34 | assertEquals(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK, intent.flags)
35 | }
36 |
37 | @Suppress("DEPRECATION")
38 | @Test
39 | fun `test getInstallAPKIntent for Android N to O`() {
40 | whenever(fileUriProvider.getFileUri(context, apk, Build.VERSION_CODES.N)).thenReturn(uri)
41 |
42 | val intent = apkIntentFactory.getInstallAPKIntent(context, apk, Build.VERSION_CODES.N)
43 |
44 | verify(fileUriProvider, times(1)).getFileUri(context, apk, Build.VERSION_CODES.N)
45 | assertEquals(Intent.ACTION_INSTALL_PACKAGE, intent.action)
46 | assertEquals(uri, intent.data)
47 | assertEquals(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK, intent.flags)
48 | }
49 |
50 | @Test
51 | fun `test getInstallAPKIntent for Android M and below`() {
52 | whenever(fileUriProvider.getFileUri(context, apk, Build.VERSION_CODES.M)).thenReturn(uri)
53 |
54 | val intent = apkIntentFactory.getInstallAPKIntent(context, apk, Build.VERSION_CODES.M)
55 |
56 | verify(fileUriProvider, times(1)).getFileUri(context, apk, Build.VERSION_CODES.M)
57 | assertEquals(Intent.ACTION_VIEW, intent.action)
58 | assertEquals(uri, intent.data)
59 | assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, intent.flags)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/FileUriProviderTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.os.Build
6 | import androidx.core.content.FileProvider
7 | import org.junit.Assert.assertEquals
8 | import org.junit.Before
9 | import org.junit.Test
10 | import org.mockito.Mockito
11 | import org.mockito.Mockito.mock
12 | import org.mockito.kotlin.whenever
13 | import java.io.File
14 |
15 | class FileUriProviderTest {
16 |
17 | private lateinit var fileUriProvider: FileUriProvider
18 | private lateinit var mockContext: Context
19 | private lateinit var mockApk: File
20 | private lateinit var mockUri: Uri
21 |
22 | @Before
23 | fun setup() {
24 | fileUriProvider = FileUriProvider()
25 | mockContext = mock(Context::class.java)
26 | mockApk = mock(File::class.java)
27 | mockUri = mock(Uri::class.java)
28 | }
29 |
30 | @Test
31 | fun `getFileUri returns file Uri for Android M`() {
32 | // Arrange
33 | Mockito.mockStatic(Uri::class.java).use { mockedUri ->
34 | whenever(Uri.fromFile(mockApk)).thenReturn(mockUri)
35 |
36 | // Act
37 | val result = fileUriProvider.getFileUri(mockContext, mockApk, Build.VERSION_CODES.M)
38 |
39 | // Assert
40 | assertEquals(mockUri, result)
41 | mockedUri.verify { Uri.fromFile(mockApk) }
42 | }
43 | }
44 |
45 | @Test
46 | fun `getFileUri returns file Uri from FileProvider for Android N and above`() {
47 | // Arrange
48 | val packageName = "com.pouyaheydari.appupdater"
49 | whenever(mockContext.packageName).thenReturn(packageName)
50 |
51 | Mockito.mockStatic(FileProvider::class.java).use { mockedFileProvider ->
52 | whenever(FileProvider.getUriForFile(mockContext, "$packageName.fileprovider", mockApk))
53 | .thenReturn(mockUri)
54 |
55 | // Act
56 | val result = fileUriProvider.getFileUri(mockContext, mockApk, Build.VERSION_CODES.N)
57 |
58 | // Assert
59 | assertEquals(mockUri, result)
60 | mockedFileProvider.verify {
61 | FileProvider.getUriForFile(mockContext, "$packageName.fileprovider", mockApk)
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallAPKIntentForMTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.mockito.kotlin.mock
9 | import org.robolectric.RobolectricTestRunner
10 |
11 | @RunWith(RobolectricTestRunner::class)
12 | class InstallAPKIntentForMTest {
13 |
14 | @Test
15 | fun testGetIntent() {
16 | val fileUri: Uri = mock()
17 | val installAPKIntentForM = InstallAPKIntentForM()
18 |
19 | val intent = installAPKIntentForM.getIntent(fileUri)
20 |
21 | assertEquals(Intent.ACTION_VIEW, intent.action)
22 | assertEquals(fileUri, intent.data)
23 | assertEquals(
24 | Intent.FLAG_ACTIVITY_NEW_TASK,
25 | intent.flags
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallAPKIntentForNToOTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.mockito.kotlin.mock
9 | import org.robolectric.RobolectricTestRunner
10 |
11 | @Suppress("DEPRECATION")
12 | @RunWith(RobolectricTestRunner::class)
13 | class InstallAPKIntentForNToOTest {
14 |
15 | @Test
16 | fun testGetIntent() {
17 | val fileUri: Uri = mock()
18 | val installAPKIntentForNToO = InstallAPKIntentForNToO()
19 |
20 | val intent = installAPKIntentForNToO.getIntent(fileUri)
21 |
22 | assertEquals(Intent.ACTION_INSTALL_PACKAGE, intent.action)
23 | assertEquals(fileUri, intent.data)
24 | assertEquals(
25 | Intent.FLAG_ACTIVITY_CLEAR_TOP or
26 | Intent.FLAG_GRANT_READ_URI_PERMISSION or
27 | Intent.FLAG_ACTIVITY_NEW_TASK,
28 | intent.flags
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallAPKIntentForPAndAboveTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.mockito.kotlin.mock
9 | import org.robolectric.RobolectricTestRunner
10 |
11 | @RunWith(RobolectricTestRunner::class)
12 | class InstallAPKIntentForPAndAboveTest {
13 |
14 | @Test
15 | fun testGetIntent() {
16 | val fileUri:Uri = mock()
17 | val installAPKIntent = InstallAPKIntentForPAndAbove()
18 | val intent = installAPKIntent.getIntent(fileUri)
19 |
20 | assertEquals(Intent.ACTION_VIEW, intent.action)
21 | assertEquals(fileUri, intent.data)
22 | assertEquals(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK, intent.flags)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/installapk/InstallApkUtilKtTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.installapk
2 |
3 | import android.content.ActivityNotFoundException
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import org.junit.Before
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.mockito.Mockito.doThrow
11 | import org.mockito.Mockito.mock
12 | import org.mockito.Mockito.verify
13 | import org.mockito.Mockito.verifyNoMoreInteractions
14 | import org.mockito.Mockito.`when`
15 | import org.mockito.kotlin.whenever
16 | import org.robolectric.RobolectricTestRunner
17 | import java.io.File
18 |
19 | @RunWith(RobolectricTestRunner::class)
20 | class InstallApkUtilTest {
21 |
22 | private val context: Context = mock()
23 | private val apkFile: File = mock()
24 | private val apkIntentFactory: APKIntentFactory = mock()
25 | private val fileUriProvider: FileUriProvider = mock()
26 | private val uri: Uri = mock()
27 |
28 | @Before
29 | fun setUp() {
30 | whenever(fileUriProvider.getFileUri(context, apkFile, 30)).thenReturn(uri)
31 | }
32 |
33 | @Test
34 | fun `installAPK should log error when APK file does not exist`() {
35 | whenever(apkFile.exists()).thenReturn(false)
36 |
37 | installAPK(context, apkFile, 30, apkIntentFactory)
38 |
39 | verify(apkFile).exists()
40 | verifyNoMoreInteractions(apkFile)
41 | }
42 |
43 | @Test
44 | fun `installAPK should start activity when APK file exists`() {
45 | `when`(apkFile.exists()).thenReturn(true)
46 | val intent = mock(Intent::class.java)
47 | `when`(apkIntentFactory.getInstallAPKIntent(context, apkFile, 30)).thenReturn(intent)
48 |
49 | installAPK(context, apkFile, 30, apkIntentFactory)
50 |
51 | verify(apkFile).exists()
52 | verify(apkIntentFactory).getInstallAPKIntent(context, apkFile, 30)
53 | verify(context).startActivity(intent)
54 | }
55 |
56 | @Test
57 | fun `installAPK should log error when ActivityNotFoundException is thrown`() {
58 | `when`(apkFile.exists()).thenReturn(true)
59 | val intent = mock(Intent::class.java)
60 | `when`(apkIntentFactory.getInstallAPKIntent(context, apkFile, 30)).thenReturn(intent)
61 | doThrow(ActivityNotFoundException::class.java).`when`(context).startActivity(intent)
62 |
63 | installAPK(context, apkFile, 30, apkIntentFactory)
64 |
65 | verify(apkFile).exists()
66 | verify(apkIntentFactory).getInstallAPKIntent(context, apkFile, 30)
67 | verify(context).startActivity(intent)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.permission
2 |
3 | import android.os.Build
4 | import org.junit.Assert.assertTrue
5 | import org.junit.Test
6 |
7 | class DownloadAPKPermissionFactoryTest {
8 |
9 | private val factory = DownloadAPKPermissionFactory()
10 |
11 | @Test
12 | fun `getDownloadAPKPermissionHandler should return DownloadAPKPermissionForPAndAbove for Android P and above`() {
13 | // Arrange
14 | val androidSdkVersion = Build.VERSION_CODES.P
15 |
16 | // Act
17 | val result = factory.getDownloadAPKPermissionHandler(androidSdkVersion)
18 |
19 | // Assert
20 | assertTrue(result is DownloadAPKPermissionForPAndAbove)
21 | }
22 |
23 | @Test
24 | fun `getDownloadAPKPermissionHandler should return DownloadAPKPermissionForOAndBellow for Android O and below`() {
25 | // Arrange
26 | val androidSdkVersion = Build.VERSION_CODES.O
27 |
28 | // Act
29 | val result = factory.getDownloadAPKPermissionHandler(androidSdkVersion)
30 |
31 | // Assert
32 | assertTrue(result is DownloadAPKPermissionForOAndBellow)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForOAndBellowTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.permission
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.pm.PackageManager
6 | import androidx.core.app.ActivityCompat
7 | import androidx.core.content.ContextCompat
8 | import org.junit.Assert.assertFalse
9 | import org.junit.Assert.assertTrue
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.mockito.Mockito.mockStatic
13 | import org.mockito.kotlin.mock
14 | import org.mockito.kotlin.whenever
15 | import org.robolectric.RobolectricTestRunner
16 |
17 | @RunWith(RobolectricTestRunner::class)
18 | class DownloadAPKPermissionForOAndBellowTest {
19 |
20 | private val mockActivity: Activity = mock()
21 | private val permissionHandler = DownloadAPKPermissionForOAndBellow()
22 |
23 | @Test
24 | fun `resolvePermissions should return true when permission is granted`() {
25 | // Arrange
26 | whenever(ContextCompat.checkSelfPermission(mockActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE))
27 | .thenReturn(PackageManager.PERMISSION_GRANTED)
28 |
29 | // Act
30 | val result = permissionHandler.resolvePermissions(mockActivity)
31 |
32 | // Assert
33 | assertTrue(result)
34 | }
35 |
36 | @Test
37 | fun `resolvePermissions should return false and request permission when permission is not granted`() {
38 | // Arrange
39 | mockStatic(ActivityCompat::class.java).use { activityCompatMock ->
40 | whenever(ContextCompat.checkSelfPermission(mockActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE))
41 | .thenReturn(PackageManager.PERMISSION_DENIED)
42 |
43 | // Act
44 | val result = permissionHandler.resolvePermissions(mockActivity)
45 |
46 | // Assert
47 | assertFalse(result)
48 | activityCompatMock.verify {
49 | ActivityCompat.requestPermissions(
50 | mockActivity,
51 | arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
52 | 2000
53 | )
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForPAndAboveTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.permission
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.net.Uri
7 | import android.provider.Settings
8 | import org.junit.Assert.assertFalse
9 | import org.junit.Assert.assertTrue
10 | import org.junit.Test
11 | import org.mockito.Mockito.any
12 | import org.mockito.Mockito.doReturn
13 | import org.mockito.Mockito.never
14 | import org.mockito.Mockito.spy
15 | import org.mockito.kotlin.mock
16 | import org.mockito.kotlin.verify
17 | import org.mockito.kotlin.whenever
18 |
19 | class DownloadAPKPermissionForPAndAboveTest {
20 |
21 | private val mockActivity: Activity = mock()
22 | private val mockPackageManager: PackageManager = mock()
23 | private val permissionHandler = DownloadAPKPermissionForPAndAbove()
24 |
25 | @Test
26 | fun `resolvePermissions should return true when canRequestPackageInstalls is true`() {
27 | whenever(mockActivity.packageManager).thenReturn(mockPackageManager)
28 | whenever(mockActivity.packageManager.canRequestPackageInstalls()).thenReturn(true)
29 |
30 | // Act
31 | val result = permissionHandler.resolvePermissions(mockActivity)
32 |
33 | // Assert
34 | assertTrue(result)
35 | verify(mockActivity.packageManager).canRequestPackageInstalls()
36 | verify(mockActivity, never()).startActivity(any())
37 | }
38 |
39 | @Test
40 | fun `resolvePermissions should return false and request permission when canRequestPackageInstalls is false`() {
41 | val mockUri = mock()
42 |
43 | whenever(mockActivity.packageManager).thenReturn(mockPackageManager)
44 | whenever(mockPackageManager.canRequestPackageInstalls()).thenReturn(false)
45 | val permissionHandler = spy(DownloadAPKPermissionForPAndAbove())
46 | doReturn(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, mockUri))
47 | .whenever(permissionHandler).getInstallFromUnknownSourceIntent(mockActivity)
48 |
49 | // Act
50 | val result = permissionHandler.resolvePermissions(mockActivity)
51 |
52 | // Assert
53 | assertFalse(result)
54 | verify(mockActivity.packageManager).canRequestPackageInstalls()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/directdownload/src/test/kotlin/com/pouyaheydari/appupdater/directdownload/utils/permission/DownloadAPKPermissionForPAndAboveUnknownSourceMethodTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.directdownload.utils.permission
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.provider.Settings
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 | import org.robolectric.RobolectricTestRunner
10 | import org.robolectric.RuntimeEnvironment
11 |
12 | @RunWith(RobolectricTestRunner::class)
13 | class DownloadAPKPermissionForPAndAboveUnknownSourceMethodTest {
14 |
15 | private val permissionHandler = DownloadAPKPermissionForPAndAbove()
16 | private val context = RuntimeEnvironment.getApplication()
17 |
18 | @Test
19 | fun `getInstallFromUnknownSourceIntent should return intent with correct action, URI, and flags`() {
20 | // Arrange
21 | val expectedPackageName = context.packageName
22 | val expectedUri = Uri.parse("package:$expectedPackageName")
23 | val expectedAction = Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
24 | val expectedFlags = Intent.FLAG_ACTIVITY_NEW_TASK
25 |
26 | // Act
27 | val intent = permissionHandler.getInstallFromUnknownSourceIntent(context)
28 |
29 | // Assert
30 | assertEquals(expectedAction, intent.action)
31 | assertEquals(expectedUri, intent.data)
32 | assertEquals(expectedFlags, intent.flags)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Dfile.encoding\=UTF-8 -XX\:+UseParallelGC -Xmx10G
2 | kotlin.code.style=official
3 | android.enableAppCompileTimeRClass=true
4 | android.nonTransitiveRClass=true
5 | android.nonFinalResIds=true
6 | android.useAndroidX=true
7 | org.gradle.parallel=true
8 | POM_GROUP_ID=com.pouyaheydari.updater
9 | POM_VERSION=10.0.0
10 | POM_DESCRIPTION=App Updater is an easy-to-use and fully customizable library to show update dialog to users.
11 | POM_URL=https://github.com/HeyPouya/AndroidAppUpdater
12 | POM_SCM_URL=https://github.com/HeyPouya/AndroidAppUpdater
13 | POM_SCM_CONNECTION=scm:git:git://github.com/HeyPouya/AndroidAppUpdater.git
14 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/HeyPouya/AndroidAppUpdater.git
15 | POM_LICENCE_NAME=The Apache License, Version 2.0
16 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
17 | POM_LICENCE_DIST=repo
18 | POM_DEVELOPER_ID=HeyPouya
19 | POM_DEVELOPER_NAME=Pouya Heydari
20 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4 | validateDistributionUrl=true
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
3 | install:
4 | - ./gradlew assemble :core:publishToMavenLocal
5 | - ./gradlew assemble :store:publishToMavenLocal
6 | - ./gradlew assemble :directdownload:publishToMavenLocal
7 | - ./gradlew assemble :compose:publishToMavenLocal
8 | - ./gradlew assemble :appupdater:publishToMavenLocal
9 |
--------------------------------------------------------------------------------
/pics/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/pics/header.png
--------------------------------------------------------------------------------
/pics/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/pics/icon.png
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ],
6 | "schedule": [
7 | "on friday"
8 | ],
9 | "timezone": "Europe/Berlin",
10 | "labels": [
11 | "dependency-update"
12 | ],
13 | "prHourlyLimit": 0,
14 | "baseBranches": [
15 | "master"
16 | ],
17 | "separateMultipleMajor": true,
18 | "dependencyDashboardTitle": "automated dependency updates dashboard",
19 | "dependencyDashboard": true,
20 | "branchPrefix": "chore/",
21 | "additionalBranchPrefix": "update-libs/",
22 | "commitMessageAction": "update",
23 | "commitMessageExtra": "{{{currentValue}}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}"
24 | }
25 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
18 |
19 | include(":app")
20 |
21 | include(":core")
22 |
23 | include(":store")
24 | include(":directdownload")
25 |
26 | include(":compose")
27 | include(":appupdater")
28 |
--------------------------------------------------------------------------------
/store/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/store/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | alias(libs.plugins.maven.publish)
5 | }
6 |
7 | android {
8 | compileSdk = libs.versions.compileSdkVersion.get().toInt()
9 | defaultConfig {
10 | minSdk = libs.versions.minSdkVersion.get().toInt()
11 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
12 | }
13 |
14 | compileOptions {
15 | sourceCompatibility = JavaVersion.VERSION_17
16 | targetCompatibility = JavaVersion.VERSION_17
17 | }
18 |
19 | kotlinOptions {
20 | jvmTarget = JavaVersion.VERSION_17.toString()
21 | }
22 | namespace = "com.pouyaheydari.appupdater.store"
23 | }
24 | java {
25 | sourceCompatibility = JavaVersion.VERSION_17
26 | targetCompatibility = JavaVersion.VERSION_17
27 | }
28 | dependencies {
29 | api(projects.core)
30 |
31 | implementation(libs.androidx.core)
32 |
33 | // testing
34 | testImplementation(libs.junit4)
35 | testImplementation(libs.roboelectric)
36 | androidTestImplementation(libs.mockito.kotlin)
37 | androidTestImplementation(libs.mockito.android)
38 | androidTestImplementation(libs.androidx.test.junit)
39 | androidTestImplementation(libs.androidx.test.rules)
40 | androidTestImplementation(libs.androidx.test.ui.espresso.core)
41 | androidTestImplementation(libs.androidx.test.ui.espresso.intents)
42 | }
43 |
--------------------------------------------------------------------------------
/store/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=store
2 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/StoreManagerTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain
2 |
3 | import android.content.ActivityNotFoundException
4 | import android.content.Context
5 | import androidx.test.espresso.intent.Intents
6 | import androidx.test.espresso.intent.matcher.IntentMatchers
7 | import androidx.test.espresso.intent.rule.IntentsRule
8 | import androidx.test.ext.junit.runners.AndroidJUnit4
9 | import androidx.test.platform.app.InstrumentationRegistry
10 | import com.pouyaheydari.appupdater.store.domain.stores.GooglePlayStore
11 | import org.junit.Rule
12 | import org.junit.Test
13 | import org.junit.runner.RunWith
14 | import org.mockito.kotlin.any
15 | import org.mockito.kotlin.mock
16 | import org.mockito.kotlin.verify
17 | import org.mockito.kotlin.whenever
18 |
19 | @RunWith(AndroidJUnit4::class)
20 | class StoreManagerTest {
21 | @get:Rule
22 | val intentsTestRule = IntentsRule()
23 |
24 | @Test
25 | fun test_showAppInSelectedStore_sendsCorrectIntent() {
26 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
27 | val packageName = "PackageName"
28 | val store = GooglePlayStore(packageName)
29 | val errorCallBack: (AppStoreCallback) -> Unit = {}
30 |
31 | showAppInSelectedStore(appContext, store, errorCallBack)
32 |
33 | Intents.intended(IntentMatchers.filterEquals(store.getIntent()))
34 | }
35 |
36 | @Test
37 | fun test_showAppInSelectedStore_storeNotInstalled_invokesErrorCallback() {
38 | val errorCallBack: (AppStoreCallback) -> Unit = mock()
39 | val store = GooglePlayStore("PackageName")
40 | val context: Context = mock()
41 | whenever(context.startActivity(any())).thenThrow(ActivityNotFoundException::class.java)
42 |
43 | showAppInSelectedStore(context, store, errorCallBack)
44 |
45 | verify(errorCallBack).invoke(any())
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/AmazonAppStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | class AmazonAppStoreTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_showAppInSelectedStore_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.AMAZON_APP_STORE, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasPackage(AMAZON_PACKAGE))
31 | Intents.intended(IntentMatchers.hasData(Uri.parse("$AMAZON_APP_STORE_URL$packageName")))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/AptoideTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class AptoideTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.APTOIDE, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$APTOIDE_URL$packageName")))
31 | Intents.intended(IntentMatchers.hasPackage(APTOIDE_PACKAGE))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/CafeBazaarStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class CafeBazaarStoreTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.CAFE_BAZAAR, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasPackage(BAZAAR_PACKAGE))
31 | Intents.intended(IntentMatchers.hasData(Uri.parse("$BAZAAR_URL$packageName")))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/FDroidTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class FDroidTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.FDROID, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$FDROID_URL$packageName")))
31 | Intents.intended(IntentMatchers.hasPackage(FDROID_PACKAGE))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/GooglePlayStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class GooglePlayStoreTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasPackage(PLAY_PACKAGE))
31 | Intents.intended(IntentMatchers.hasData(Uri.parse("$PLAY_URL$packageName")))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/HuaweiAppGalleryTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class HuaweiAppGalleryTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.HUAWEI_APP_GALLERY, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$HUAWEI_APP_GALLERY_URL$packageName")))
31 | Intents.intended(IntentMatchers.hasPackage(HUAWEI_APP_GALLERY_PACKAGE))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/LenovoAppCenterTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class LenovoAppCenterTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.LENOVO_APP_CENTER, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$LENOVO_APP_CENTER_URL$packageName")))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/MiGetAppStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class MiGetAppStoreTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.MI_GET_APP_STORE, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$MI_APP_STORE_URL$packageName")))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/MyketStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class MyketStoreTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.MYKET, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasPackage(MYKET_PACKAGE))
31 | Intents.intended(IntentMatchers.hasData(Uri.parse("$MYKET_URL$packageName")))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/NineAppsTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class NineAppsTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.NINE_APPS_STORE, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasPackage(NINE_APPS_PACKAGE))
31 | Intents.intended(IntentMatchers.hasData(Uri.parse("$NINE_APPS_STORE_URL$packageName")))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/OneStoreAppMarketTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class OneStoreAppMarketTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.ONE_STORE_APP_MARKET, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$ONE_STORE_APP_MARKET_URL$packageName")))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/OppoAppMarketTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class OppoAppMarketTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.OPPO_APP_MARKET, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasPackage(OPPO_PACKAGE))
31 | Intents.intended(IntentMatchers.hasData(Uri.parse("$OPPO_APP_MARKET_URL$packageName")))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/SamsungGalaxyStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class SamsungGalaxyStoreTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.SAMSUNG_GALAXY_STORE, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$SAMSUNG_GALAXY_STORE_URL$packageName")))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/TencentStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class TencentStoreTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.TENCENT_APPS_STORE, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$TENCENT_APP_STORE_URL$packageName")))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/VAppStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class VAppStoreTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.V_APP_STORE, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$V_APP_STORE_URL$packageName")))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/store/src/androidTest/java/com/pouyaheydari/appupdater/store/domain/stores/ZTEAppCenterTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.net.Uri
4 | import androidx.test.espresso.intent.Intents
5 | import androidx.test.espresso.intent.matcher.IntentMatchers
6 | import androidx.test.espresso.intent.rule.IntentsRule
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import com.pouyaheydari.appupdater.store.domain.AppStoreCallback
10 | import com.pouyaheydari.appupdater.store.domain.StoreFactory
11 | import com.pouyaheydari.appupdater.store.domain.showAppInSelectedStore
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | internal class ZTEAppCenterTest {
18 | @get:Rule
19 | val intentsTestRule = IntentsRule()
20 |
21 | @Test
22 | fun whenCalling_setStoreData_then_intentGetsFiredCorrectly() {
23 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24 | val packageName = appContext.packageName
25 | val store = StoreFactory.getStore(AppStoreType.ZTE_APP_CENTER, packageName)
26 | val errorCallback: (AppStoreCallback) -> Unit = {}
27 |
28 | showAppInSelectedStore(appContext, store, errorCallback)
29 |
30 | Intents.intended(IntentMatchers.hasData(Uri.parse("$ZTE_APP_CENTER_URL$packageName")))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/AppStoreCallback.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain
2 |
3 | import android.content.ActivityNotFoundException
4 | import com.pouyaheydari.appupdater.store.domain.stores.AppStore
5 |
6 | sealed interface AppStoreCallback {
7 | data class Success(val store: AppStore) : AppStoreCallback
8 | data class Failure(val store: AppStore, val exception: ActivityNotFoundException) : AppStoreCallback
9 | }
10 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreIntentBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 |
6 | internal object StoreIntentBuilder {
7 | internal class Builder(private val uriString: String) {
8 | private var storePackageName: String? = null
9 |
10 | fun withPackage(storePackageName: String): Builder {
11 | require(storePackageName.isNotBlank()) { "Store's package name most not be empty" }
12 | this.storePackageName = storePackageName
13 | return this
14 | }
15 |
16 | fun build(): Intent = Intent(Intent.ACTION_VIEW, Uri.parse(uriString)).apply {
17 | flags = Intent.FLAG_ACTIVITY_NEW_TASK
18 | storePackageName?.takeIf { it.isNotBlank() }?.let { setPackage(it) }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreListItem.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import androidx.core.os.ParcelCompat
6 | import com.pouyaheydari.appupdater.store.R
7 | import com.pouyaheydari.appupdater.store.domain.stores.AppStore
8 | import com.pouyaheydari.appupdater.store.domain.stores.AppStoreType
9 |
10 | data class StoreListItem(
11 | var store: AppStore = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, ""),
12 | var title: String = "",
13 | var icon: Int = R.drawable.appupdater_ic_cloud,
14 | ) : Parcelable {
15 | private constructor(parcel: Parcel) : this(
16 | ParcelCompat.readParcelable(parcel, AppStore::class.java.classLoader, AppStore::class.java) ?: StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, ""),
17 | parcel.readString().orEmpty(),
18 | parcel.readInt(),
19 | )
20 |
21 | override fun writeToParcel(parcel: Parcel, flags: Int) {
22 | parcel.writeParcelable(store, flags)
23 | parcel.writeString(title)
24 | parcel.writeInt(icon)
25 | }
26 |
27 | override fun describeContents(): Int = 0
28 |
29 | companion object CREATOR : Parcelable.Creator {
30 | override fun createFromParcel(parcel: Parcel): StoreListItem {
31 | return StoreListItem(parcel)
32 | }
33 |
34 | override fun newArray(size: Int): Array {
35 | return arrayOfNulls(size)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/StoreManager.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain
2 |
3 | import android.content.ActivityNotFoundException
4 | import android.content.Context
5 | import com.pouyaheydari.appupdater.store.domain.stores.AppStore
6 |
7 | fun showAppInSelectedStore(context: Context?, store: AppStore, callback: (AppStoreCallback) -> Unit) {
8 | try {
9 | val intent = store.getIntent()
10 | context?.startActivity(intent)
11 | callback(AppStoreCallback.Success(store))
12 | } catch (exception: ActivityNotFoundException) {
13 | callback(AppStoreCallback.Failure(store, exception))
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AmazonAppStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val AMAZON_APP_STORE_URL = "amzn://apps/android?p="
8 | internal const val AMAZON_PACKAGE = "com.amazon.venezia"
9 |
10 | /**
11 | * Opens application's page in [Amazon App Store](https://www.amazon.com/gp/mas/get/amazonapp)
12 | */
13 | internal data class AmazonAppStore(val packageName: String) : AppStore {
14 | private constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() =
17 | StoreIntentBuilder
18 | .Builder("$AMAZON_APP_STORE_URL$packageName")
19 | .withPackage(AMAZON_PACKAGE)
20 | .build()
21 |
22 | override fun getType(): AppStoreType = AppStoreType.AMAZON_APP_STORE
23 |
24 | override fun getUserReadableName(): String = AppStoreType.AMAZON_APP_STORE.userReadableName
25 |
26 | override fun writeToParcel(parcel: Parcel, flags: Int) {
27 | parcel.writeString(packageName)
28 | }
29 |
30 | override fun describeContents() = 0
31 |
32 | companion object CREATOR : Parcelable.Creator {
33 | override fun createFromParcel(parcel: Parcel): AmazonAppStore {
34 | return AmazonAppStore(parcel)
35 | }
36 |
37 | override fun newArray(size: Int): Array {
38 | return arrayOfNulls(size)
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.content.Intent
4 | import android.os.Parcelable
5 |
6 | interface AppStore : Parcelable {
7 | fun getIntent(): Intent
8 | fun getType(): AppStoreType
9 | fun getUserReadableName(): String
10 | }
11 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/AppStoreType.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | enum class AppStoreType(internal val userReadableName: String) {
4 | GOOGLE_PLAY("Google Play"),
5 | CAFE_BAZAAR("Cafe Bazaar"),
6 | MYKET("Myket"),
7 | HUAWEI_APP_GALLERY("App Gallery"),
8 | SAMSUNG_GALAXY_STORE("Galaxy Store"),
9 | AMAZON_APP_STORE("Amazon App Store"),
10 | APTOIDE("Aptoide"),
11 | FDROID("Fdroid"),
12 | MI_GET_APP_STORE("Get App Store"),
13 | ONE_STORE_APP_MARKET("One Store"),
14 | OPPO_APP_MARKET("App Market"),
15 | V_APP_STORE("V App Store"),
16 | NINE_APPS_STORE("Nine Apps"),
17 | TENCENT_APPS_STORE("Tencent App Store"),
18 | ZTE_APP_CENTER("ZTE App Center"),
19 | LENOVO_APP_CENTER("Lenovo App Center"),
20 | }
21 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/Aptoide.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val APTOIDE_URL = "aptoideinstall://package="
8 | internal const val APTOIDE_PACKAGE = "cm.aptoide.pt"
9 |
10 | /**
11 | * Opens application's page in [Aptoide App Store](https://en.aptoide.com/)
12 | */
13 | internal data class Aptoide(val packageName: String) : AppStore {
14 | private constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() = StoreIntentBuilder
17 | .Builder("$APTOIDE_URL$packageName")
18 | .withPackage(APTOIDE_PACKAGE)
19 | .build()
20 |
21 | override fun getType(): AppStoreType = AppStoreType.APTOIDE
22 |
23 | override fun getUserReadableName(): String = AppStoreType.APTOIDE.userReadableName
24 |
25 | override fun writeToParcel(parcel: Parcel, flags: Int) {
26 | parcel.writeString(packageName)
27 | }
28 |
29 | override fun describeContents() = 0
30 |
31 | companion object CREATOR : Parcelable.Creator {
32 | override fun createFromParcel(parcel: Parcel): Aptoide {
33 | return Aptoide(parcel)
34 | }
35 |
36 | override fun newArray(size: Int): Array {
37 | return arrayOfNulls(size)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/CafeBazaarStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val BAZAAR_URL = "bazaar://details?id="
8 | internal const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
9 |
10 | /**
11 | * Opens application's page in [CafeBazaar App Store](https://cafebazaar.ir)
12 | */
13 | internal data class CafeBazaarStore(val packageName: String) : AppStore {
14 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() = StoreIntentBuilder
17 | .Builder("$BAZAAR_URL$packageName")
18 | .withPackage(BAZAAR_PACKAGE)
19 | .build()
20 |
21 | override fun getType(): AppStoreType = AppStoreType.CAFE_BAZAAR
22 |
23 | override fun getUserReadableName(): String = AppStoreType.CAFE_BAZAAR.userReadableName
24 |
25 | override fun writeToParcel(parcel: Parcel, flags: Int) {
26 | parcel.writeString(packageName)
27 | }
28 |
29 | override fun describeContents() = 0
30 |
31 | companion object CREATOR : Parcelable.Creator {
32 | override fun createFromParcel(parcel: Parcel): CafeBazaarStore {
33 | return CafeBazaarStore(parcel)
34 | }
35 |
36 | override fun newArray(size: Int): Array {
37 | return arrayOfNulls(size)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/FDroid.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val FDROID_URL = "fdroid.app://details?id="
8 | internal const val FDROID_PACKAGE = "org.fdroid.fdroid"
9 |
10 | /**
11 | * Opens application's page in [F-Droid App Store](https://f-droid.org/)
12 | */
13 | internal data class FDroid(val packageName: String) : AppStore {
14 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() = StoreIntentBuilder
17 | .Builder("$FDROID_URL$packageName")
18 | .withPackage(FDROID_PACKAGE)
19 | .build()
20 |
21 | override fun getType(): AppStoreType = AppStoreType.FDROID
22 |
23 | override fun getUserReadableName(): String = AppStoreType.FDROID.userReadableName
24 |
25 | override fun writeToParcel(parcel: Parcel, flags: Int) {
26 | parcel.writeString(packageName)
27 | }
28 |
29 | override fun describeContents() = 0
30 |
31 | companion object CREATOR : Parcelable.Creator {
32 | override fun createFromParcel(parcel: Parcel): FDroid {
33 | return FDroid(parcel)
34 | }
35 |
36 | override fun newArray(size: Int): Array {
37 | return arrayOfNulls(size)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/GooglePlayStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val PLAY_URL = "market://details?id="
8 | internal const val PLAY_PACKAGE = "com.android.vending"
9 |
10 | /**
11 | * Opens application's page in [GooglePlay Store](https://play.google.com)
12 | */
13 | internal data class GooglePlayStore(val packageName: String) : AppStore {
14 | private constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() = StoreIntentBuilder
17 | .Builder("$PLAY_URL$packageName")
18 | .withPackage(PLAY_PACKAGE)
19 | .build()
20 |
21 | override fun getType(): AppStoreType = AppStoreType.GOOGLE_PLAY
22 |
23 | override fun getUserReadableName(): String = AppStoreType.GOOGLE_PLAY.userReadableName
24 |
25 | override fun writeToParcel(parcel: Parcel, flags: Int) {
26 | parcel.writeString(packageName)
27 | }
28 |
29 | override fun describeContents() = 0
30 |
31 | companion object CREATOR : Parcelable.Creator {
32 | override fun createFromParcel(parcel: Parcel): GooglePlayStore {
33 | return GooglePlayStore(parcel)
34 | }
35 |
36 | override fun newArray(size: Int): Array {
37 | return arrayOfNulls(size)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/HuaweiAppGallery.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val HUAWEI_APP_GALLERY_URL = "appmarket://details?id="
8 | internal const val HUAWEI_APP_GALLERY_PACKAGE = "com.huawei.appmarket"
9 |
10 | /**
11 | * Opens application's page in [Huawei App Gallery](https://appgallery.huawei.com/)
12 | */
13 | internal data class HuaweiAppGallery(val packageName: String) : AppStore {
14 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() = StoreIntentBuilder
17 | .Builder("$HUAWEI_APP_GALLERY_URL$packageName")
18 | .withPackage(HUAWEI_APP_GALLERY_PACKAGE)
19 | .build()
20 |
21 | override fun getType(): AppStoreType = AppStoreType.HUAWEI_APP_GALLERY
22 |
23 | override fun getUserReadableName(): String = AppStoreType.HUAWEI_APP_GALLERY.userReadableName
24 |
25 | override fun writeToParcel(parcel: Parcel, flags: Int) {
26 | parcel.writeString(packageName)
27 | }
28 |
29 | override fun describeContents() = 0
30 |
31 | companion object CREATOR : Parcelable.Creator {
32 | override fun createFromParcel(parcel: Parcel): HuaweiAppGallery {
33 | return HuaweiAppGallery(parcel)
34 | }
35 |
36 | override fun newArray(size: Int): Array {
37 | return arrayOfNulls(size)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/LenovoAppCenter.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val LENOVO_APP_CENTER_URL = "leapp://ptn/appinfo.do?pn="
8 |
9 | /**
10 | * Opens application's page in [Lenovo App Store](https://www.lenovomm.com/)
11 | */
12 | internal data class LenovoAppCenter(val packageName: String) : AppStore {
13 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
14 |
15 | override fun getIntent() = StoreIntentBuilder
16 | .Builder("$LENOVO_APP_CENTER_URL$packageName")
17 | .build()
18 |
19 | override fun getType(): AppStoreType = AppStoreType.LENOVO_APP_CENTER
20 |
21 | override fun getUserReadableName(): String = AppStoreType.LENOVO_APP_CENTER.userReadableName
22 |
23 | override fun writeToParcel(parcel: Parcel, flags: Int) {
24 | parcel.writeString(packageName)
25 | }
26 |
27 | override fun describeContents() = 0
28 |
29 | companion object CREATOR : Parcelable.Creator {
30 | override fun createFromParcel(parcel: Parcel): LenovoAppCenter {
31 | return LenovoAppCenter(parcel)
32 | }
33 |
34 | override fun newArray(size: Int): Array {
35 | return arrayOfNulls(size)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MiGetAppStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val MI_APP_STORE_URL = "mimarket://details?id="
8 |
9 | /**
10 | * Opens application's page in [Xiaomi GetApp store](https://global.app.mi.com/)
11 | */
12 | internal data class MiGetAppStore(val packageName: String) : AppStore {
13 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
14 |
15 | override fun getIntent() =
16 | StoreIntentBuilder
17 | .Builder("$MI_APP_STORE_URL$packageName")
18 | .build()
19 |
20 | override fun getType(): AppStoreType = AppStoreType.MI_GET_APP_STORE
21 |
22 | override fun getUserReadableName(): String = AppStoreType.MI_GET_APP_STORE.userReadableName
23 |
24 | override fun writeToParcel(parcel: Parcel, flags: Int) {
25 | parcel.writeString(packageName)
26 | }
27 |
28 | override fun describeContents() = 0
29 |
30 | companion object CREATOR : Parcelable.Creator {
31 | override fun createFromParcel(parcel: Parcel): MiGetAppStore {
32 | return MiGetAppStore(parcel)
33 | }
34 |
35 | override fun newArray(size: Int): Array {
36 | return arrayOfNulls(size)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/MyketStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val MYKET_URL = "myket://details?id="
8 | internal const val MYKET_PACKAGE = "ir.mservices.market"
9 |
10 | /**
11 | * Opens application's page in [Myket Store](https://myket.ir/)
12 | */
13 | internal data class MyketStore(val packageName: String) : AppStore {
14 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() = StoreIntentBuilder
17 | .Builder("$MYKET_URL$packageName")
18 | .withPackage(MYKET_PACKAGE)
19 | .build()
20 |
21 | override fun getType(): AppStoreType = AppStoreType.MYKET
22 |
23 | override fun getUserReadableName(): String = AppStoreType.MYKET.userReadableName
24 |
25 | override fun writeToParcel(parcel: Parcel, flags: Int) {
26 | parcel.writeString(packageName)
27 | }
28 |
29 | override fun describeContents() = 0
30 |
31 | companion object CREATOR : Parcelable.Creator {
32 | override fun createFromParcel(parcel: Parcel): MyketStore {
33 | return MyketStore(parcel)
34 | }
35 |
36 | override fun newArray(size: Int): Array {
37 | return arrayOfNulls(size)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/NineApps.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val NINE_APPS_STORE_URL = "nineapps://AppDetail?id="
8 | internal const val NINE_APPS_PACKAGE = "com.gamefun.apk2u"
9 |
10 | /**
11 | * Opens application's page in [9-Apps](https://www.9apps.com/)
12 | */
13 | internal data class NineApps(val packageName: String) : AppStore {
14 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() = StoreIntentBuilder
17 | .Builder("$NINE_APPS_STORE_URL$packageName")
18 | .withPackage(NINE_APPS_PACKAGE)
19 | .build()
20 |
21 | override fun getType(): AppStoreType = AppStoreType.NINE_APPS_STORE
22 |
23 | override fun getUserReadableName(): String = AppStoreType.NINE_APPS_STORE.userReadableName
24 |
25 | override fun writeToParcel(parcel: Parcel, flags: Int) {
26 | parcel.writeString(packageName)
27 | }
28 |
29 | override fun describeContents() = 0
30 |
31 | companion object CREATOR : Parcelable.Creator {
32 | override fun createFromParcel(parcel: Parcel): NineApps {
33 | return NineApps(parcel)
34 | }
35 |
36 | override fun newArray(size: Int): Array {
37 | return arrayOfNulls(size)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OneStoreAppMarket.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val ONE_STORE_APP_MARKET_URL = "onestore://common/product/"
8 |
9 | /**
10 | * Opens application's page in [OneStore App Market](https://m.onestore.co.kr/mobilepoc/main/main.omp)
11 | */
12 | internal data class OneStoreAppMarket(val packageName: String) : AppStore {
13 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
14 |
15 | override fun getIntent() =
16 | StoreIntentBuilder
17 | .Builder("$ONE_STORE_APP_MARKET_URL$packageName")
18 | .build()
19 |
20 | override fun getType(): AppStoreType = AppStoreType.ONE_STORE_APP_MARKET
21 |
22 | override fun getUserReadableName(): String = AppStoreType.ONE_STORE_APP_MARKET.userReadableName
23 |
24 | override fun writeToParcel(parcel: Parcel, flags: Int) {
25 | parcel.writeString(packageName)
26 | }
27 |
28 | override fun describeContents() = 0
29 |
30 | companion object CREATOR : Parcelable.Creator {
31 | override fun createFromParcel(parcel: Parcel): OneStoreAppMarket {
32 | return OneStoreAppMarket(parcel)
33 | }
34 |
35 | override fun newArray(size: Int): Array {
36 | return arrayOfNulls(size)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/OppoAppMarket.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val OPPO_APP_MARKET_URL = "market://details?id="
8 | internal const val OPPO_PACKAGE = "com.heytap.market"
9 |
10 | /**
11 | * Opens application's page in [OppoAppMarket](https://oppomobile.com/)
12 | */
13 | internal data class OppoAppMarket(val packageName: String) : AppStore {
14 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
15 |
16 | override fun getIntent() = StoreIntentBuilder
17 | .Builder("$OPPO_APP_MARKET_URL$packageName")
18 | .withPackage(OPPO_PACKAGE)
19 | .build()
20 |
21 | override fun getType(): AppStoreType = AppStoreType.OPPO_APP_MARKET
22 |
23 | override fun getUserReadableName(): String = AppStoreType.OPPO_APP_MARKET.userReadableName
24 |
25 | override fun writeToParcel(parcel: Parcel, flags: Int) {
26 | parcel.writeString(packageName)
27 | }
28 |
29 | override fun describeContents() = 0
30 |
31 | companion object CREATOR : Parcelable.Creator {
32 | override fun createFromParcel(parcel: Parcel): OppoAppMarket {
33 | return OppoAppMarket(parcel)
34 | }
35 |
36 | override fun newArray(size: Int): Array {
37 | return arrayOfNulls(size)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/SamsungGalaxyStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val SAMSUNG_GALAXY_STORE_URL = "samsungapps://ProductDetail/"
8 |
9 | /**
10 | * Opens application's page in [Samsung Galaxy store](https://www.samsung.com/de/apps/galaxy-store/)
11 | */
12 | internal data class SamsungGalaxyStore(val packageName: String) : AppStore {
13 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
14 |
15 | override fun getIntent() = StoreIntentBuilder
16 | .Builder("$SAMSUNG_GALAXY_STORE_URL$packageName")
17 | .build()
18 |
19 | override fun getType(): AppStoreType = AppStoreType.SAMSUNG_GALAXY_STORE
20 |
21 | override fun getUserReadableName(): String = AppStoreType.SAMSUNG_GALAXY_STORE.userReadableName
22 |
23 | override fun writeToParcel(parcel: Parcel, flags: Int) {
24 | parcel.writeString(packageName)
25 | }
26 |
27 | override fun describeContents() = 0
28 |
29 | companion object CREATOR : Parcelable.Creator {
30 | override fun createFromParcel(parcel: Parcel): SamsungGalaxyStore {
31 | return SamsungGalaxyStore(parcel)
32 | }
33 |
34 | override fun newArray(size: Int): Array {
35 | return arrayOfNulls(size)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/TencentAppStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val TENCENT_APP_STORE_URL = "tmast://appdetails?pname="
8 |
9 | /**
10 | * Opens application's page in [Tencent App Store](https://appstore.tencent.com/)
11 | */
12 | internal data class TencentAppStore(val packageName: String) : AppStore {
13 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
14 |
15 | override fun getIntent() = StoreIntentBuilder
16 | .Builder("$TENCENT_APP_STORE_URL$packageName")
17 | .build()
18 |
19 | override fun getType(): AppStoreType = AppStoreType.TENCENT_APPS_STORE
20 |
21 | override fun getUserReadableName(): String = AppStoreType.TENCENT_APPS_STORE.userReadableName
22 |
23 | override fun writeToParcel(parcel: Parcel, flags: Int) {
24 | parcel.writeString(packageName)
25 | }
26 |
27 | override fun describeContents() = 0
28 |
29 | companion object CREATOR : Parcelable.Creator {
30 | override fun createFromParcel(parcel: Parcel): TencentAppStore {
31 | return TencentAppStore(parcel)
32 | }
33 |
34 | override fun newArray(size: Int): Array {
35 | return arrayOfNulls(size)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/VAppStore.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val V_APP_STORE_URL = "vivoMarket://details?id="
8 |
9 | /**
10 | * Opens application's page in [V-AppStore](https://developer.vivo.com/home)
11 | */
12 | internal data class VAppStore(val packageName: String) : AppStore {
13 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
14 |
15 | override fun getIntent() = StoreIntentBuilder
16 | .Builder("$V_APP_STORE_URL$packageName")
17 | .build()
18 |
19 | override fun getType(): AppStoreType = AppStoreType.V_APP_STORE
20 |
21 | override fun getUserReadableName(): String = AppStoreType.V_APP_STORE.userReadableName
22 |
23 | override fun writeToParcel(parcel: Parcel, flags: Int) {
24 | parcel.writeString(packageName)
25 | }
26 |
27 | override fun describeContents() = 0
28 |
29 | companion object CREATOR : Parcelable.Creator {
30 | override fun createFromParcel(parcel: Parcel): VAppStore {
31 | return VAppStore(parcel)
32 | }
33 |
34 | override fun newArray(size: Int): Array {
35 | return arrayOfNulls(size)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/store/src/main/java/com/pouyaheydari/appupdater/store/domain/stores/ZTEAppCenter.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain.stores
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.pouyaheydari.appupdater.store.domain.StoreIntentBuilder
6 |
7 | internal const val ZTE_APP_CENTER_URL = "zte_market://appdetails?pname="
8 |
9 | /**
10 | * Opens application's page in [ZTE App Store](https://apps.ztems.com/)
11 | */
12 | internal data class ZTEAppCenter(val packageName: String) : AppStore {
13 | constructor(parcel: Parcel) : this(parcel.readString().orEmpty())
14 |
15 | override fun getIntent() = StoreIntentBuilder
16 | .Builder("$ZTE_APP_CENTER_URL$packageName")
17 | .build()
18 |
19 | override fun getType(): AppStoreType = AppStoreType.ZTE_APP_CENTER
20 |
21 | override fun getUserReadableName(): String = AppStoreType.ZTE_APP_CENTER.userReadableName
22 |
23 | override fun writeToParcel(parcel: Parcel, flags: Int) {
24 | parcel.writeString(packageName)
25 | }
26 |
27 | override fun describeContents() = 0
28 |
29 | companion object CREATOR : Parcelable.Creator {
30 | override fun createFromParcel(parcel: Parcel): ZTEAppCenter {
31 | return ZTEAppCenter(parcel)
32 | }
33 |
34 | override fun newArray(size: Int): Array {
35 | return arrayOfNulls(size)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_amazon_app_store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_amazon_app_store.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_bazar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_bazar.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_cloud.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_fdroid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_fdroid.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_galaxy_store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_galaxy_store.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_get_app_store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_get_app_store.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_lenovo_app_center.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_lenovo_app_center.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_nine_apps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_nine_apps.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_one_store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_one_store.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_oppo_app_market.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_oppo_app_market.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_tencent_app_store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_tencent_app_store.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_v_app_store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_v_app_store.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable-nodpi/appupdater_ic_zte_app_center.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HeyPouya/AndroidAppUpdater/d7da0e6440876887bfdc6c1b613964f5ccd78df6/store/src/main/res/drawable-nodpi/appupdater_ic_zte_app_center.png
--------------------------------------------------------------------------------
/store/src/main/res/drawable/appupdater_ic_app_gallery.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
40 |
41 |
--------------------------------------------------------------------------------
/store/src/main/res/drawable/appupdater_ic_aptoide.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/store/src/main/res/drawable/appupdater_ic_google_play.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/store/src/main/res/drawable/appupdater_ic_myket.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/store/src/test/java/com/pouyaheydari/appupdater/store/domain/StoreIntentBuilderTest.kt:
--------------------------------------------------------------------------------
1 | package com.pouyaheydari.appupdater.store.domain
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Assert.assertNull
7 | import org.junit.Assert.assertThrows
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.robolectric.RobolectricTestRunner
11 |
12 | @RunWith(RobolectricTestRunner::class)
13 | class StoreIntentBuilderTest {
14 | @Test
15 | fun `happy path`() {
16 | val uriString = "https://pouyaheydari.com"
17 | val packageName = "PackageName"
18 |
19 | val intent = StoreIntentBuilder.Builder(uriString).withPackage(packageName).build()
20 |
21 | assertEquals(Intent.ACTION_VIEW, intent.action)
22 | assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, intent.flags)
23 | assertEquals(packageName, intent.`package`)
24 | assertEquals(Uri.parse(uriString), intent.data)
25 | }
26 |
27 | @Test
28 | fun `whenever the uri is valid and no package name is provided, then the correct Intent is returned`() {
29 | val uriString = "https://pouyaheydari.com"
30 |
31 | val intent = StoreIntentBuilder.Builder(uriString).build()
32 |
33 | assertEquals(Intent.ACTION_VIEW, intent.action)
34 | assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, intent.flags)
35 | assertEquals(Uri.parse(uriString), intent.data)
36 | assertNull(intent.`package`)
37 | }
38 |
39 | @Test
40 | fun `whenever a blank package name is set, then IllegalArgumentException is thrown`() {
41 | val uriString = "https://pouyaheydari.com"
42 | val packageName = ""
43 | val builder = StoreIntentBuilder.Builder(uriString)
44 |
45 | assertThrows(IllegalArgumentException::class.java) {
46 | builder.withPackage(packageName)
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------