Loading .gitlab-ci.yml +8 −6 Original line number Diff line number Diff line image: registry.gitlab.e.foundation/e/os/docker-android-apps-cicd:55-workshop-auto-release image: registry.gitlab.e.foundation/e/os/docker-android-apps-cicd:master variables: SENTRY_DSN: $SENTRY_DSN Loading @@ -22,8 +22,10 @@ buildDebug: - app/build/outputs/apk/debug/ test: allow_failure: true stage: debug stage: release rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' when: always script: - ./gradlew test -PtestAccountName="$testAccountName" -PtestAccountPwd="$testAccountPwd" -PtestServerUrl="$testServerUrl" artifacts: Loading app/build.gradle +8 −6 Original line number Diff line number Diff line Loading @@ -10,8 +10,8 @@ plugins { } def versionMajor = 2 def versionMinor = 5 def versionPatch = 5 def versionMinor = 6 def versionPatch = 0 def getGitHash = { -> def stdOut = new ByteArrayOutputStream() Loading Loading @@ -119,17 +119,19 @@ android { } buildFeatures { viewBinding true aidl true } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '11' jvmTarget = '17' } lint { lintConfig file('lint.xml') } namespace 'foundation.e.apps' kotlin.sourceSets.all { languageSettings.optIn("kotlin.RequiresOptIn") } Loading Loading @@ -159,7 +161,7 @@ dependencies { // TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 api files('libs/splitinstall-lib.jar') implementation 'foundation.e.lib:telemetry:0.0.9-alpha' implementation 'foundation.e.lib:telemetry:0.0.11-alpha' implementation 'foundation.e:gplayapi:3.0.1-2' implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.1' Loading app/src/main/AndroidManifest.xml +2 −3 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="foundation.e.apps"> xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> Loading Loading @@ -46,7 +45,7 @@ android:usesCleartextTraffic="true"> <activity android:name=".ui.MainActivity" android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> Loading app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +4 −1 Original line number Diff line number Diff line Loading @@ -19,7 +19,9 @@ package foundation.e.apps import android.app.Application import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import androidx.work.ExistingPeriodicWorkPolicy Loading Loading @@ -56,13 +58,14 @@ class AppLoungeApplication : Application(), Configuration.Provider { @Inject lateinit var preferenceManagerModule: PreferenceManagerModule @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onCreate() { super.onCreate() InstallWorkManager.context = this // Register broadcast receiver for package manager val pkgManagerBR = object : PkgManagerBR() {} registerReceiver(pkgManagerBR, pkgManagerModule.getFilter()) registerReceiver(pkgManagerBR, pkgManagerModule.getFilter(), RECEIVER_EXPORTED) val currentVersion = dataStoreModule.getTOSVersion() if (!currentVersion.contentEquals(TOS_VERSION)) { Loading app/src/main/java/foundation/e/apps/ui/MainActivity.kt→app/src/main/java/foundation/e/apps/MainActivity.kt +52 −81 Original line number Diff line number Diff line Loading @@ -16,14 +16,10 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.ui package foundation.e.apps import android.app.usage.StorageStatsManager import android.os.Build import android.os.Bundle import android.os.Environment import android.os.StatFs import android.os.storage.StorageManager import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle Loading @@ -42,18 +38,17 @@ import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.install.updates.UpdatesNotifier import foundation.e.apps.install.workmanager.InstallWorkManager import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections import foundation.e.apps.ui.settings.SettingsFragment import foundation.e.apps.ui.setup.signin.SignInViewModel import foundation.e.apps.utils.SystemInfoProvider import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus Loading @@ -61,12 +56,10 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import java.util.UUID @AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var signInViewModel: SignInViewModel private lateinit var loginViewModel: LoginViewModel private lateinit var binding: ActivityMainBinding private val TAG = MainActivity::class.java.simpleName Loading @@ -88,6 +81,7 @@ class MainActivity : AppCompatActivity() { var hasInternet = true viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] // navOptions and activityNavController for TOS and SignIn Fragments Loading Loading @@ -170,19 +164,8 @@ class MainActivity : AppCompatActivity() { viewModel.createNotificationChannels() } // Observe and handle downloads viewModel.downloadList.observe(this) { list -> list.forEach { if (it.status == Status.QUEUED) { handleFusedDownloadQueued(it, viewModel) } } } viewModel.purchaseAppLiveData.observe(this) { val action = AppPurchaseFragmentDirections.actionGlobalAppPurchaseFragment(it.packageName) findNavController(R.id.fragment).navigate(action) goToAppPurchaseFragment(it) } viewModel.errorMessage.observe(this) { Loading Loading @@ -227,10 +210,54 @@ class MainActivity : AppCompatActivity() { launch { observeSignatureMissMatchError() } launch { observerErrorEvent() } launch { observeAppPurchaseFragment() } launch { observeNoInternetEvent() } } } } private suspend fun observeNoInternetEvent() { EventBus.events.filter { appEvent -> appEvent is AppEvent.NoInternetEvent }.collectLatest { if (!(it.data as Boolean)) { showNoInternet() } } } private suspend fun observeAppPurchaseFragment() { EventBus.events.filter { appEvent -> appEvent is AppEvent.AppPurchaseEvent }.collectLatest { goToAppPurchaseFragment(it.data as FusedDownload) } } private fun goToAppPurchaseFragment(it: FusedDownload) { val action = AppPurchaseFragmentDirections.actionGlobalAppPurchaseFragment(it.packageName) findNavController(R.id.fragment).navigate(action) } private suspend fun observerErrorEvent() { EventBus.events.filter { appEvent -> appEvent is AppEvent.ErrorMessageEvent }.collectLatest { showSnackbarMessage(getString(it.data as Int)) } } private suspend fun observeSignatureMissMatchError() { EventBus.events.filter { appEvent -> appEvent is AppEvent.SignatureMissMatchError Loading Loading @@ -281,35 +308,13 @@ class MainActivity : AppCompatActivity() { } } private fun handleFusedDownloadQueued( it: FusedDownload, viewModel: MainActivityViewModel ) { lifecycleScope.launch { if (!isStorageAvailable(it)) { showSnackbarMessage(getString(R.string.not_enough_storage)) viewModel.updateUnAvailable(it) return@launch } if (viewModel.internetConnection.value == false) { showNoInternet() viewModel.updateUnAvailable(it) return@launch } viewModel.updateAwaiting(it) InstallWorkManager.enqueueWork(it) Timber.d("===> onCreate: AWAITING ${it.name}") } } private fun startInstallationOfPurchasedApp( viewModel: MainActivityViewModel, it: String packageName: String ) { lifecycleScope.launch { val fusedDownload = viewModel.updateAwaitingForPurchasedApp(it) val fusedDownload = viewModel.updateAwaitingForPurchasedApp(packageName) if (fusedDownload != null) { InstallWorkManager.enqueueWork(fusedDownload) ApplicationDialogFragment( title = getString(R.string.purchase_complete), message = getString(R.string.download_automatically_message), Loading @@ -333,38 +338,4 @@ class MainActivity : AppCompatActivity() { binding.noInternet.visibility = View.VISIBLE binding.fragment.visibility = View.GONE } // TODO: move storage availability code to FileManager Class private fun isStorageAvailable(fusedDownload: FusedDownload): Boolean { val availableSpace = calculateAvailableDiskSpace() return availableSpace > fusedDownload.appSize + (500 * (1000 * 1000)) } private fun calculateAvailableDiskSpace(): Long { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val storageManager = getSystemService(STORAGE_SERVICE) as StorageManager val statsManager = getSystemService(STORAGE_STATS_SERVICE) as StorageStatsManager val uuid = storageManager.primaryStorageVolume.uuid try { if (uuid != null) { statsManager.getFreeBytes(UUID.fromString(uuid)) } else { statsManager.getFreeBytes(StorageManager.UUID_DEFAULT) } } catch (e: Exception) { Timber.e("calculateAvailableDiskSpace: ${e.stackTraceToString()}") getAvailableInternalMemorySize() } } else { getAvailableInternalMemorySize() } } private fun getAvailableInternalMemorySize(): Long { val path: File = Environment.getDataDirectory() val stat = StatFs(path.path) val blockSize = stat.blockSizeLong val availableBlocks = stat.availableBlocksLong return availableBlocks * blockSize } } Loading
.gitlab-ci.yml +8 −6 Original line number Diff line number Diff line image: registry.gitlab.e.foundation/e/os/docker-android-apps-cicd:55-workshop-auto-release image: registry.gitlab.e.foundation/e/os/docker-android-apps-cicd:master variables: SENTRY_DSN: $SENTRY_DSN Loading @@ -22,8 +22,10 @@ buildDebug: - app/build/outputs/apk/debug/ test: allow_failure: true stage: debug stage: release rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' when: always script: - ./gradlew test -PtestAccountName="$testAccountName" -PtestAccountPwd="$testAccountPwd" -PtestServerUrl="$testServerUrl" artifacts: Loading
app/build.gradle +8 −6 Original line number Diff line number Diff line Loading @@ -10,8 +10,8 @@ plugins { } def versionMajor = 2 def versionMinor = 5 def versionPatch = 5 def versionMinor = 6 def versionPatch = 0 def getGitHash = { -> def stdOut = new ByteArrayOutputStream() Loading Loading @@ -119,17 +119,19 @@ android { } buildFeatures { viewBinding true aidl true } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '11' jvmTarget = '17' } lint { lintConfig file('lint.xml') } namespace 'foundation.e.apps' kotlin.sourceSets.all { languageSettings.optIn("kotlin.RequiresOptIn") } Loading Loading @@ -159,7 +161,7 @@ dependencies { // TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 api files('libs/splitinstall-lib.jar') implementation 'foundation.e.lib:telemetry:0.0.9-alpha' implementation 'foundation.e.lib:telemetry:0.0.11-alpha' implementation 'foundation.e:gplayapi:3.0.1-2' implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.1' Loading
app/src/main/AndroidManifest.xml +2 −3 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="foundation.e.apps"> xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> Loading Loading @@ -46,7 +45,7 @@ android:usesCleartextTraffic="true"> <activity android:name=".ui.MainActivity" android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> Loading
app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +4 −1 Original line number Diff line number Diff line Loading @@ -19,7 +19,9 @@ package foundation.e.apps import android.app.Application import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import androidx.work.ExistingPeriodicWorkPolicy Loading Loading @@ -56,13 +58,14 @@ class AppLoungeApplication : Application(), Configuration.Provider { @Inject lateinit var preferenceManagerModule: PreferenceManagerModule @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onCreate() { super.onCreate() InstallWorkManager.context = this // Register broadcast receiver for package manager val pkgManagerBR = object : PkgManagerBR() {} registerReceiver(pkgManagerBR, pkgManagerModule.getFilter()) registerReceiver(pkgManagerBR, pkgManagerModule.getFilter(), RECEIVER_EXPORTED) val currentVersion = dataStoreModule.getTOSVersion() if (!currentVersion.contentEquals(TOS_VERSION)) { Loading
app/src/main/java/foundation/e/apps/ui/MainActivity.kt→app/src/main/java/foundation/e/apps/MainActivity.kt +52 −81 Original line number Diff line number Diff line Loading @@ -16,14 +16,10 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.ui package foundation.e.apps import android.app.usage.StorageStatsManager import android.os.Build import android.os.Bundle import android.os.Environment import android.os.StatFs import android.os.storage.StorageManager import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle Loading @@ -42,18 +38,17 @@ import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.install.updates.UpdatesNotifier import foundation.e.apps.install.workmanager.InstallWorkManager import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections import foundation.e.apps.ui.settings.SettingsFragment import foundation.e.apps.ui.setup.signin.SignInViewModel import foundation.e.apps.utils.SystemInfoProvider import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus Loading @@ -61,12 +56,10 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import java.util.UUID @AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var signInViewModel: SignInViewModel private lateinit var loginViewModel: LoginViewModel private lateinit var binding: ActivityMainBinding private val TAG = MainActivity::class.java.simpleName Loading @@ -88,6 +81,7 @@ class MainActivity : AppCompatActivity() { var hasInternet = true viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] // navOptions and activityNavController for TOS and SignIn Fragments Loading Loading @@ -170,19 +164,8 @@ class MainActivity : AppCompatActivity() { viewModel.createNotificationChannels() } // Observe and handle downloads viewModel.downloadList.observe(this) { list -> list.forEach { if (it.status == Status.QUEUED) { handleFusedDownloadQueued(it, viewModel) } } } viewModel.purchaseAppLiveData.observe(this) { val action = AppPurchaseFragmentDirections.actionGlobalAppPurchaseFragment(it.packageName) findNavController(R.id.fragment).navigate(action) goToAppPurchaseFragment(it) } viewModel.errorMessage.observe(this) { Loading Loading @@ -227,10 +210,54 @@ class MainActivity : AppCompatActivity() { launch { observeSignatureMissMatchError() } launch { observerErrorEvent() } launch { observeAppPurchaseFragment() } launch { observeNoInternetEvent() } } } } private suspend fun observeNoInternetEvent() { EventBus.events.filter { appEvent -> appEvent is AppEvent.NoInternetEvent }.collectLatest { if (!(it.data as Boolean)) { showNoInternet() } } } private suspend fun observeAppPurchaseFragment() { EventBus.events.filter { appEvent -> appEvent is AppEvent.AppPurchaseEvent }.collectLatest { goToAppPurchaseFragment(it.data as FusedDownload) } } private fun goToAppPurchaseFragment(it: FusedDownload) { val action = AppPurchaseFragmentDirections.actionGlobalAppPurchaseFragment(it.packageName) findNavController(R.id.fragment).navigate(action) } private suspend fun observerErrorEvent() { EventBus.events.filter { appEvent -> appEvent is AppEvent.ErrorMessageEvent }.collectLatest { showSnackbarMessage(getString(it.data as Int)) } } private suspend fun observeSignatureMissMatchError() { EventBus.events.filter { appEvent -> appEvent is AppEvent.SignatureMissMatchError Loading Loading @@ -281,35 +308,13 @@ class MainActivity : AppCompatActivity() { } } private fun handleFusedDownloadQueued( it: FusedDownload, viewModel: MainActivityViewModel ) { lifecycleScope.launch { if (!isStorageAvailable(it)) { showSnackbarMessage(getString(R.string.not_enough_storage)) viewModel.updateUnAvailable(it) return@launch } if (viewModel.internetConnection.value == false) { showNoInternet() viewModel.updateUnAvailable(it) return@launch } viewModel.updateAwaiting(it) InstallWorkManager.enqueueWork(it) Timber.d("===> onCreate: AWAITING ${it.name}") } } private fun startInstallationOfPurchasedApp( viewModel: MainActivityViewModel, it: String packageName: String ) { lifecycleScope.launch { val fusedDownload = viewModel.updateAwaitingForPurchasedApp(it) val fusedDownload = viewModel.updateAwaitingForPurchasedApp(packageName) if (fusedDownload != null) { InstallWorkManager.enqueueWork(fusedDownload) ApplicationDialogFragment( title = getString(R.string.purchase_complete), message = getString(R.string.download_automatically_message), Loading @@ -333,38 +338,4 @@ class MainActivity : AppCompatActivity() { binding.noInternet.visibility = View.VISIBLE binding.fragment.visibility = View.GONE } // TODO: move storage availability code to FileManager Class private fun isStorageAvailable(fusedDownload: FusedDownload): Boolean { val availableSpace = calculateAvailableDiskSpace() return availableSpace > fusedDownload.appSize + (500 * (1000 * 1000)) } private fun calculateAvailableDiskSpace(): Long { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val storageManager = getSystemService(STORAGE_SERVICE) as StorageManager val statsManager = getSystemService(STORAGE_STATS_SERVICE) as StorageStatsManager val uuid = storageManager.primaryStorageVolume.uuid try { if (uuid != null) { statsManager.getFreeBytes(UUID.fromString(uuid)) } else { statsManager.getFreeBytes(StorageManager.UUID_DEFAULT) } } catch (e: Exception) { Timber.e("calculateAvailableDiskSpace: ${e.stackTraceToString()}") getAvailableInternalMemorySize() } } else { getAvailableInternalMemorySize() } } private fun getAvailableInternalMemorySize(): Long { val path: File = Environment.getDataDirectory() val stat = StatFs(path.path) val blockSize = stat.blockSizeLong val availableBlocks = stat.availableBlocksLong return availableBlocks * blockSize } }