diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55a276a24a07b969b95ccd0d138232f8da5f836c..d3263795769e99961bf06ea8e363ab70a2ceba82 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -141,6 +141,8 @@ telemetry = { module = "foundation.e.lib:telemetry", version.ref = "telemetry" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } truth = { module = "com.google.truth:truth", version.ref = "truth" } viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } +ui = { group = "androidx.compose.ui", name = "ui" } +ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } work-testing = { group = "androidx.work", name = "work-testing", version.ref = "workTesting" } diff --git a/install-app-lib/installappdemo/.gitignore b/install-app-lib/installappdemo/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /dev/null +++ b/install-app-lib/installappdemo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/install-app-lib/installappdemo/build.gradle.kts b/install-app-lib/installappdemo/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..753c589430cca0d1b342605ecb1541134de8e392 --- /dev/null +++ b/install-app-lib/installappdemo/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) +} + +android { + namespace = "foundation.e.apps.installapp.demo" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "foundation.e.apps.installapp.demo" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + + signingConfigs { + create("platformConfig") { + storeFile = file("../../app/keystore/platform.jks") + storePassword = "platform" + keyAlias = "platform" + keyPassword = "platform" + } + } + + buildTypes { + debug { + signingConfig = signingConfigs.get("platformConfig") + } + + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get()) + } + kotlinOptions { + jvmTarget = libs.versions.jvmTarget.get() + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(project(":install-app-lib")) +// implementation("foundation.e.apps:install-app-lib:1.0.0") + + implementation(libs.core.ktx) + implementation(libs.lifecycle.runtime.ktx) + implementation(libs.activity.compose) + implementation(platform(libs.compose.bom)) + implementation(libs.ui) + implementation(libs.ui.graphics) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + debugImplementation(libs.compose.ui.tooling) + debugImplementation(libs.compose.ui.test.manifest) +} diff --git a/install-app-lib/installappdemo/src/main/AndroidManifest.xml b/install-app-lib/installappdemo/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..3dd3efdb7cffe5f0f7b693c9a04032512e39e2f8 --- /dev/null +++ b/install-app-lib/installappdemo/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/install-app-lib/installappdemo/src/main/java/foundation/e/apps/installapp/demo/MainActivity.kt b/install-app-lib/installappdemo/src/main/java/foundation/e/apps/installapp/demo/MainActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..32daded8463573d6eed094bc4061d667d21b9e65 --- /dev/null +++ b/install-app-lib/installappdemo/src/main/java/foundation/e/apps/installapp/demo/MainActivity.kt @@ -0,0 +1,162 @@ +package foundation.e.apps.installapp.demo + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.unit.dp +import androidx.lifecycle.lifecycleScope +import foundation.e.apps.installapp.AppInstaller +import foundation.e.apps.installapp.AppLoungeConfiguration +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +class MainActivity : ComponentActivity() { + private val progress = MutableStateFlow("Not-connected") + private val installStatus = MutableStateFlow(null) + private val appLoungeConfigurationFlow = MutableStateFlow(null) + private var installJob: Job? = null + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + InstallAppScreen( + modifier = Modifier.padding(innerPadding), + appLoungeConfigurationFlow, + progress, + installStatus, + ::installApp, + ::stopInstallApp + ) + } + } + + getAppLoungeConfiguration() + } + + private fun installApp(packageName: String) { + installStatus.value = null + val appInstaller = AppInstaller(this) + + installJob = lifecycleScope.launch { + installStatus.value = appInstaller.installByPackageName(packageName, { + progress.value = it.name + }).name + } + } + + private fun stopInstallApp() { + installJob?.cancel() + } + + private fun getAppLoungeConfiguration() { + val appInstaller = AppInstaller(this) + lifecycleScope.launch { + appLoungeConfigurationFlow.value = appInstaller.getAppLoungeConfiguration() + } + } +} + +@Composable() +fun InstallAppScreen( + modifier: Modifier, + appLoungeConfigurationFlow: Flow, + statusFlow: Flow, + installStatusFlow: Flow, + installApp: (String) -> Unit, + stopInstallApp: () -> Unit +) { + + var packageName by remember { mutableStateOf("") } + val status by statusFlow.collectAsState("not connected") + val installStatus by installStatusFlow.collectAsState(null) + val appLoungeConfiguration by appLoungeConfigurationFlow.collectAsState(null) + + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = spacedBy(8.dp) + ) { + Text("AppLounge configuration") + appLoungeConfiguration?.let { + Text("GPlayAccountType:") + Text( + it.gPlayAccountType.name, + Modifier.layoutId("GPlayAccountType") + ) + Text("SearchableSources:") + Text( + it.searchableSources.joinToString("-"), + Modifier.layoutId("SearchableSources"), + ) + } + + TextField( + value = packageName, + onValueChange = { packageName = it }, + label = { Text(text = "Package name") }, + modifier = Modifier.fillMaxWidth() + ) + + Button( + onClick = { installApp(packageName) }, + modifier = Modifier.fillMaxWidth().padding(top = 12.dp) + ) { + Text("Install") + } + + Button( + onClick = { + stopInstallApp() + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + ) { + Text("STOP") + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + ) { + Text("Status: ") + Text(status, Modifier.layoutId("status")) + } + installStatus?.let { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + ) { + Text("Install status: ") + Text(it, Modifier.layoutId("InstallStatus")) + } + } + } +} diff --git a/install-app-lib/installappdemo/src/main/res/drawable/app_icon.xml b/install-app-lib/installappdemo/src/main/res/drawable/app_icon.xml new file mode 100644 index 0000000000000000000000000000000000000000..51bc4f2137e6a9146b80eb09e53881e30238ed00 --- /dev/null +++ b/install-app-lib/installappdemo/src/main/res/drawable/app_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/maestro/install-app-lib/abort_install_apk.yaml b/maestro/install-app-lib/abort_install_apk.yaml new file mode 100644 index 0000000000000000000000000000000000000000..de4147b86312878d7cdd5bc43902d25ec2e9f370 --- /dev/null +++ b/maestro/install-app-lib/abort_install_apk.yaml @@ -0,0 +1,15 @@ +# Scenario: install-app-lib abort with EXPECTED_STATE on install app by PACKAGE_NAME +# Given InstallAppDemo is installed +# When user set PACKAGE_NAME in packageName field +# And click Install +# Then the status finally change to EXPECTED_STATE + +appId: foundation.e.apps.installapp.demo +--- + +- launchApp +- tapOn: Package name +- inputText: ${PACKAGE_NAME} +- tapOn: Install + +- assertVisible: ${EXPECTED_STATE} diff --git a/maestro/install-app-lib/check_applounge_configuration.yaml b/maestro/install-app-lib/check_applounge_configuration.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8585903030236271ad88d54f03af85afda2c1457 --- /dev/null +++ b/maestro/install-app-lib/check_applounge_configuration.yaml @@ -0,0 +1,12 @@ +# Scenario: install-app-lib show AppLounge configuration +# Given InstallAppDemo is installed +# When open InstallAppDemo +# Then GPlayAccountType shows GPLAY_ACCOUNT_TYPE +# And SeachableSrouces shows SEARCHABLE_SOURCES + +appId: foundation.e.apps.installapp.demo +--- + +- launchApp +- assertVisible: ${GPLAY_ACCOUNT_TYPE} +- assertVisible: ${SEARCHABLE_SOURCES} diff --git a/maestro/install-app-lib/first_start_anonymous.yaml b/maestro/install-app-lib/first_start_anonymous.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9fa4013a8e5511f9b48f7ff9e3752255495cd469 --- /dev/null +++ b/maestro/install-app-lib/first_start_anonymous.yaml @@ -0,0 +1,9 @@ +appId: foundation.e.apps + +--- + +- launchApp: foundation.e.apps +- tapOn: + id: foundation.e.apps:id/agreeBT +- tapOn: + id: foundation.e.apps:id/anonymousBT diff --git a/maestro/install-app-lib/first_start_google_account.yaml b/maestro/install-app-lib/first_start_google_account.yaml new file mode 100644 index 0000000000000000000000000000000000000000..edd27259a0f61489a3d3047efc1ba986a114ecc1 --- /dev/null +++ b/maestro/install-app-lib/first_start_google_account.yaml @@ -0,0 +1,20 @@ +appId: foundation.e.apps + +--- + +- launchApp: foundation.e.apps +- tapOn: + id: foundation.e.apps:id/agreeBT +- tapOn: + id: foundation.e.apps:id/googleBT +- tapOn: PROCEED TO GOOGLE LOGIN +- tapOn: Sign in with Google in AppLounge only +- tapOn: Email or phone +- inputText: ${GOOGLE_ACCOUNT} +- tapOn: Next +- extendedWaitUntil: + visible: Show password + timeout: 10000 +- inputText: ${GOOGLE_PASSWORD} +- tapOn: Next +- tapOn: I agree diff --git a/maestro/install-app-lib/first_start_nogoogle.yaml b/maestro/install-app-lib/first_start_nogoogle.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3bf81347b7ae82ae814260dc2996776d89782330 --- /dev/null +++ b/maestro/install-app-lib/first_start_nogoogle.yaml @@ -0,0 +1,9 @@ +appId: foundation.e.apps + +--- + +- launchApp: foundation.e.apps +- tapOn: + id: foundation.e.apps:id/agreeBT +- tapOn: + id: foundation.e.apps:id/noGoogleBT diff --git a/maestro/install-app-lib/install-cleanapk-app.yaml b/maestro/install-app-lib/install-cleanapk-app.yaml new file mode 100644 index 0000000000000000000000000000000000000000..139a9e350feed7ed390fc949436c09164f3b12d6 --- /dev/null +++ b/maestro/install-app-lib/install-cleanapk-app.yaml @@ -0,0 +1,11 @@ +appId: foundation.e.apps.installapp.demo +env: + APK: "com.woefe.shoppinglist" + # "com.woefe.shoppinglist", "com.icedblueberry.shoppinglisteasy") +--- + +- launchApp: foundation.e.apps.installapp.demo +- tapOn: Package name +- inputText: com.woefe.shoppinglist +- tapOn: Install +- assertVisible: INSTALLED diff --git a/maestro/install-app-lib/install_apk.yaml b/maestro/install-app-lib/install_apk.yaml new file mode 100644 index 0000000000000000000000000000000000000000..05b6f627b5c8d7ea43a7cdbd5032e71e0330327e --- /dev/null +++ b/maestro/install-app-lib/install_apk.yaml @@ -0,0 +1,23 @@ +# Scenario: Install app by PACKAGE_NAME through install-app-lib +# Given AppLounge is configured with anonymous or Google account +# And InstallAppDemo is installed +# And App with PACKAGE_NAME is not installed +# When user set PACKAGE_NAME in packageName field +# And click Install +# Then the app starts to download +# And status move to DOWNLOADING, INSTALLING and finally INSTALLED + +appId: foundation.e.apps.installapp.demo +#env: +# PACKAGE_NAME: "com.woefe.shoppinglist" +# # "com.woefe.shoppinglist", "com.icedblueberry.shoppinglisteasy") +--- + +- launchApp +- tapOn: Package name +- inputText: ${PACKAGE_NAME} +- tapOn: Install + +- assertVisible: DOWNLOADING +- assertVisible: INSTALLING +- assertVisible: INSTALLED diff --git a/maestro/install-app-lib/run_cleanapk_tests.sh b/maestro/install-app-lib/run_cleanapk_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..179e3f93b5c3a983f981516349c0f7a3c036c931 --- /dev/null +++ b/maestro/install-app-lib/run_cleanapk_tests.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +# Wake up the device if screen black (not password configured) +screen_info=`adb shell dumpsys power | grep mHoldingDisplaySuspendBlocker` +if [[ $screen_info == *mHoldingDisplaySuspendBlocker=false* ]] +then + adb shell input keyevent 26 +fi + +adb shell input keyevent 82 + +adb shell svc wifi enable +## End wake up device. + +adb shell pm clear foundation.e.apps + +maestro test -e GPLAY_ACCOUNT_TYPE=NOT_CONFIGURED -e SEARCHABLE_SOURCES=PLAY_STORE-OPEN_SOURCE-PWA check_applounge_configuration.yaml || exit 1 + +# Scenario: Return UNAVAILABLE when AppLounge not ready yet +maestro test -e PACKAGE_NAME=no.an.existing.packangename -e EXPECTED_STATE=UNAVAILABLE abort_install_apk.yaml || exit 1 + +# Scenario: Setup AppLounge with anonymous Google account +maestro test first_start_nogoogle.yaml || exit 1 + +# Scenario: AppLounge should be configured with no google now +maestro test -e GPLAY_ACCOUNT_TYPE=NO_GOOGLE -e SEARCHABLE_SOURCES=OPEN_SOURCE-PWA check_applounge_configuration.yaml || exit 1 + +# Scenario: Install FDroid App through install-app-lib +adb uninstall com.woefe.shoppinglist +maestro test -e PACKAGE_NAME=com.woefe.shoppinglist install_apk.yaml || exit 1 + +# Scenario: Return UNAVAILABLE when app unavailable on stores +maestro test -e PACKAGE_NAME=no.an.existing.packangename -e EXPECTED_STATE=UNAVAILABLE abort_install_apk.yaml || exit 1 + +# Scenario: Install PWA App through install-app-lib +maestro test -e PACKAGE_NAME=dice.richardekwonye.com -e EXPECTED_STATE=INSTALLED abort_install_apk.yaml || exit 1 + +# Scenario: Return error when no network available +adb shell svc wifi disable +maestro test -e PACKAGE_NAME=no.an.existing.packangename -e EXPECTED_STATE=INSTALLATION_ISSUE abort_install_apk.yaml || exit 1 + +adb shell svc wifi enable diff --git a/maestro/install-app-lib/run_playstore_tests.sh b/maestro/install-app-lib/run_playstore_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..43ea9d122b6d59cca1b4aae9805eb7b4305acf9c --- /dev/null +++ b/maestro/install-app-lib/run_playstore_tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Usage: with plugged in device, with AppLounge and applibdemo installed, +# $ run_playstore_test GOOGLE_ACCOUNT GOOGLE_PASSWORD + +cd "$(dirname "$0")" + +GOOGLE_ACCOUNT=$1 +GOOGLE_PASSWORD=$2 + +# Wake up the device if screen black (not password configured) +screen_info=`adb shell dumpsys power | grep mHoldingDisplaySuspendBlocker` +if [[ $screen_info == *mHoldingDisplaySuspendBlocker=false* ]] +then + adb shell input keyevent 26 +fi + +adb shell input keyevent 82 + +adb shell svc wifi enable +## End wake up device. + +adb shell pm clear foundation.e.apps + +# Scenario: Setup AppLounge with anonymous Google account +maestro test -e GOOGLE_ACCOUNT=$GOOGLE_ACCOUNT -e GOOGLE_PASSWORD=$GOOGLE_PASSWORD first_start_google_account.yaml || exit 1 + +maestro test -e GPLAY_ACCOUNT_TYPE=GOOGLE -e SEARCHABLE_SOURCES=PLAY_STORE-OPEN_SOURCE-PWA check_applounge_configuration.yaml || exit 1 + +# Scenario: Install PlayStore App through install-app-lib +adb uninstall com.icedblueberry.shoppinglisteasy +maestro test -e PACKAGE_NAME=com.icedblueberry.shoppinglisteasy install_apk.yaml || exit 1 + +# Scenario: Return UNAVAILABLE when app unavailable on stores +maestro test -e PACKAGE_NAME=no.an.existing.packangename -e EXPECTED_STATE=UNAVAILABLE abort_install_apk.yaml || exit 1 + +# Scenario: Return error when app not purchased in AppLounge +maestro test -e PACKAGE_NAME=com.MOBGames.PoppyMobileChap1 -e EXPECTED_STATE=PURCHASE_NEEDED abort_install_apk.yaml || exit 1 diff --git a/settings.gradle b/settings.gradle index 3da8de1876216ce600f7a46ef5089e5d22416d4b..afa5364b27997672d608dcdaf88153a97f865cb0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -65,3 +65,4 @@ include ':app' include ':parental-control-data' include ':auth-data-lib' include ':install-app-lib' +include ':install-app-lib:installappdemo'