Loading README.md +82 −39 Original line number Diff line number Diff line Loading @@ -9,19 +9,61 @@ App Lounge use the _Packaging by Features_ approach for packaging the code. A re ``` . ├── api │ ├── cleanapk │ ├── fused │ └── gplay │ ├── cleanapk │ │ ├── blockedApps │ │ └── data │ │ ├── app │ │ ├── categories │ │ ├── download │ │ ├── home │ │ └── search │ ├── database │ ├── ecloud │ │ └── modules │ ├── exodus │ │ ├── models │ │ └── repositories │ ├── fdroid │ │ └── models │ ├── fused │ │ ├── data │ │ └── utils │ └── gplay │ ├── token │ └── utils ├── application │ ├── model │ └── subFrags ├── applicationlist │ └── model ├── categories │ └── model ├── di ├── home │ └── model ├── manager │ ├── database │ │ └── fusedDownload │ ├── download │ │ └── data │ ├── fused │ ├── notification │ ├── pkg │ └── workmanager ├── purchase ├── receiver ├── search ├── settings ├── setup │ ├── signin │ │ └── google │ └── tos ├── updates │ └── manager └── utils ├── enums └── modules ``` ## API Loading @@ -30,6 +72,7 @@ App Lounge use the following APIs to offer applications: - [GPlayApi](https://gitlab.com/AuroraOSS/gplayapi) from Aurora OSS - [CleanAPK API](https://info.cleanapk.org/) from CleanAPK - [Exodus API](https://github.com/Exodus-Privacy/exodus/blob/v1/doc/api.md) from Exodus-Privacy ## Development Loading app/build.gradle +3 −0 Original line number Diff line number Diff line Loading @@ -125,6 +125,9 @@ dependencies { // implementation "com.squareup.moshi:moshi-adapters:1.5.0" implementation "com.squareup.okhttp3:okhttp:4.9.2" // JSON Converter implementation 'com.squareup.retrofit2:converter-gson:2.5.0' // YAML factory implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.2" Loading app/src/main/java/foundation/e/apps/MainActivity.kt +7 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,7 @@ import foundation.e.apps.setup.signin.SignInViewModel import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.parentFragment.TimeoutFragment import foundation.e.apps.utils.modules.CommonUtilsModule import kotlinx.coroutines.launch import java.io.File Loading Loading @@ -140,6 +141,12 @@ class MainActivity : AppCompatActivity() { generateAuthDataBasedOnUserType(user) } else { Log.d(TAG, "Timeout validating auth data!") val lastFragment = navHostFragment.childFragmentManager.fragments[0] if (lastFragment is TimeoutFragment) { Log.d(TAG, "Displaying timeout from MainActivity on fragment: " + lastFragment::class.java.name) lastFragment.onTimeout() } } } } else { Loading app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +17 −108 Original line number Diff line number Diff line Loading @@ -18,15 +18,12 @@ package foundation.e.apps import android.app.Activity import android.content.Context import android.content.DialogInterface import android.graphics.Bitmap import android.os.Build import android.os.SystemClock import android.util.Base64 import android.util.Log import android.view.KeyEvent import android.widget.ImageView import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog Loading @@ -49,7 +46,6 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.settings.SettingsFragment import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Type Loading Loading @@ -94,108 +90,6 @@ class MainActivityViewModel @Inject constructor( */ var firstAuthDataFetchTime = 0L /* * Alert dialog to show to user if App Lounge times out. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ private lateinit var timeoutAlertDialog: AlertDialog /** * Display timeout alert dialog. * * @param activity Activity class. Basically the MainActivity. * @param positiveButtonBlock Code block when "Retry" is pressed. * @param openSettings Code block when "Open Settings" button is pressed. * This should open the [SettingsFragment] fragment. * @param applicationTypeFromPreferences Application type string, can be one of * [FusedAPIImpl.APP_TYPE_ANY], [FusedAPIImpl.APP_TYPE_OPEN], [FusedAPIImpl.APP_TYPE_PWA] */ fun displayTimeoutAlertDialog( activity: Activity, positiveButtonBlock: () -> Unit, openSettings: () -> Unit, applicationTypeFromPreferences: String, ) { if (!this::timeoutAlertDialog.isInitialized) { timeoutAlertDialog = AlertDialog.Builder(activity).apply { setTitle(R.string.timeout_title) /* * Prevent dismissing the dialog from pressing outside as it will only * show a blank screen below the dialog. */ setCancelable(false) /* * If user presses back button to close the dialog without selecting anything, * close App Lounge. */ setOnKeyListener { dialog, keyCode, _ -> if (keyCode == KeyEvent.KEYCODE_BACK) { dialog.dismiss() activity.finish() } true } }.create() } timeoutAlertDialog.apply { /* * Set retry button. */ setButton(DialogInterface.BUTTON_POSITIVE, activity.getString(R.string.retry)) { _, _ -> positiveButtonBlock() } /* * Set message based on apps from GPlay of cleanapk. */ setMessage( activity.getString( when (applicationTypeFromPreferences) { FusedAPIImpl.APP_TYPE_ANY -> R.string.timeout_desc_gplay else -> R.string.timeout_desc_cleanapk } ) ) /* * Show "Open Setting" only for GPlay apps. */ if (applicationTypeFromPreferences == FusedAPIImpl.APP_TYPE_ANY) { setButton( DialogInterface.BUTTON_NEUTRAL, activity.getString(R.string.open_settings) ) { _, _ -> openSettings() } } } timeoutAlertDialog.show() } /** * Returns true if [timeoutAlertDialog] is displaying. * Returs false if it is not initialised. */ fun isTimeoutDialogDisplayed(): Boolean { return if (this::timeoutAlertDialog.isInitialized) { timeoutAlertDialog.isShowing } else false } /** * Dismisses the [timeoutAlertDialog] if it is being displayed. * Does nothing if it is not being displayed. * Caller need not check if the dialog is being displayed. */ fun dismissTimeoutDialog() { if (isTimeoutDialogDisplayed()) { try { timeoutAlertDialog.dismiss() } catch (_: Exception) {} } } // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() var installInProgress = false Loading @@ -213,8 +107,10 @@ class MainActivityViewModel @Inject constructor( } fun setFirstTokenFetchTime() { if (firstAuthDataFetchTime == 0L) { firstAuthDataFetchTime = SystemClock.uptimeMillis() } } fun isTimeEligibleForTokenRefresh(): Boolean { return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis Loading @@ -228,6 +124,7 @@ class MainActivityViewModel @Inject constructor( * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ fun retryFetchingTokenAfterTimeout() { firstAuthDataFetchTime = 0 setFirstTokenFetchTime() authValidity.postValue(false) } Loading @@ -248,7 +145,19 @@ class MainActivityViewModel @Inject constructor( if (!authRequestRunning) { authRequestRunning = true viewModelScope.launch { fusedAPIRepository.fetchAuthData() /* * If getting auth data failed, try getting again. * Sending false in authValidity, triggers observer in MainActivity, * causing it to destroy credentials and try to regenerate auth data. * * Issue: * https://gitlab.e.foundation/e/backlog/-/issues/5413 * https://gitlab.e.foundation/e/backlog/-/issues/5404 */ if (!fusedAPIRepository.fetchAuthData()) { authRequestRunning = false authValidity.postValue(false) } } } } Loading app/src/main/java/foundation/e/apps/api/JobResult.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line package foundation.e.apps.api import foundation.e.apps.utils.enums.ResultStatus /** * Currently defunct, not being used anywhere. * Prototype to merge API request and also get rid of Pair, Triple for timeout related cases. */ open class JobResult<T> private constructor(val status: ResultStatus) { /* * Classes for returning multiple data from a function along with a status * in the form of ResultStatus. * Use the static overloaded create methods (in companion object) to for easy creation. * * If needed to just pass a single data element with status for API requests, * see the static methods success(), error(), loading() (in companion object). */ class of1<A> (val data1: A, status: ResultStatus): JobResult<A>(status) class of2<A,B> (val data1: A, val data2: B, status: ResultStatus): JobResult<A>(status) class of3<A,B,C> (val data1: A, val data2: B, val data3: C, status: ResultStatus): JobResult<A>(status) var message = "" /* * This is the primary data, mainly for API requests which might send null data. * Other data (type B, C ...) are secondary/optional data. * * For non-null return type, directly use of1, of2, of3 ... classes * and directly access data1, data2, data3 ... */ val data: T? get() = when(this) { is of1 -> this.data1 is of2<T, *> -> this.data1 is of3<T, *, *> -> this.data1 else -> null } fun isSuccess(): Boolean { return status == ResultStatus.OK } companion object { fun <A> create(data1: A, status: ResultStatus, message: String? = null): of1<A> { return of1(data1, status).apply { message?.let { this.message = message } } } fun <A,B> create(data1: A, data2: B, status: ResultStatus, message: String? = null): of2<A,B> { return of2(data1, data2, status).apply { message?.let { this.message = message } } } fun <A,B,C> create(data1: A, data2: B, data3: C, status: ResultStatus, message: String? = null): of3<A,B,C> { return of3(data1, data2, data3, status).apply { message?.let { this.message = message } } } /* * Methods for API */ fun <T> success(data: T): JobResult<T> { return of1(data, ResultStatus.OK) } fun <T> error(message: String, data: T? = null): JobResult<T> { val result = if (data == null) JobResult(ResultStatus.UNKNOWN) else of1<T>(data, ResultStatus.UNKNOWN) return result.apply { this.message = message } } /*fun <T> loading(data: T?): JobResult<T> { return if (data == null) JobResult(ResultStatus.LOADING) else JobResult.of1(data, ResultStatus.LOADING) }*/ } } No newline at end of file Loading
README.md +82 −39 Original line number Diff line number Diff line Loading @@ -9,19 +9,61 @@ App Lounge use the _Packaging by Features_ approach for packaging the code. A re ``` . ├── api │ ├── cleanapk │ ├── fused │ └── gplay │ ├── cleanapk │ │ ├── blockedApps │ │ └── data │ │ ├── app │ │ ├── categories │ │ ├── download │ │ ├── home │ │ └── search │ ├── database │ ├── ecloud │ │ └── modules │ ├── exodus │ │ ├── models │ │ └── repositories │ ├── fdroid │ │ └── models │ ├── fused │ │ ├── data │ │ └── utils │ └── gplay │ ├── token │ └── utils ├── application │ ├── model │ └── subFrags ├── applicationlist │ └── model ├── categories │ └── model ├── di ├── home │ └── model ├── manager │ ├── database │ │ └── fusedDownload │ ├── download │ │ └── data │ ├── fused │ ├── notification │ ├── pkg │ └── workmanager ├── purchase ├── receiver ├── search ├── settings ├── setup │ ├── signin │ │ └── google │ └── tos ├── updates │ └── manager └── utils ├── enums └── modules ``` ## API Loading @@ -30,6 +72,7 @@ App Lounge use the following APIs to offer applications: - [GPlayApi](https://gitlab.com/AuroraOSS/gplayapi) from Aurora OSS - [CleanAPK API](https://info.cleanapk.org/) from CleanAPK - [Exodus API](https://github.com/Exodus-Privacy/exodus/blob/v1/doc/api.md) from Exodus-Privacy ## Development Loading
app/build.gradle +3 −0 Original line number Diff line number Diff line Loading @@ -125,6 +125,9 @@ dependencies { // implementation "com.squareup.moshi:moshi-adapters:1.5.0" implementation "com.squareup.okhttp3:okhttp:4.9.2" // JSON Converter implementation 'com.squareup.retrofit2:converter-gson:2.5.0' // YAML factory implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.2" Loading
app/src/main/java/foundation/e/apps/MainActivity.kt +7 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,7 @@ import foundation.e.apps.setup.signin.SignInViewModel import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.parentFragment.TimeoutFragment import foundation.e.apps.utils.modules.CommonUtilsModule import kotlinx.coroutines.launch import java.io.File Loading Loading @@ -140,6 +141,12 @@ class MainActivity : AppCompatActivity() { generateAuthDataBasedOnUserType(user) } else { Log.d(TAG, "Timeout validating auth data!") val lastFragment = navHostFragment.childFragmentManager.fragments[0] if (lastFragment is TimeoutFragment) { Log.d(TAG, "Displaying timeout from MainActivity on fragment: " + lastFragment::class.java.name) lastFragment.onTimeout() } } } } else { Loading
app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +17 −108 Original line number Diff line number Diff line Loading @@ -18,15 +18,12 @@ package foundation.e.apps import android.app.Activity import android.content.Context import android.content.DialogInterface import android.graphics.Bitmap import android.os.Build import android.os.SystemClock import android.util.Base64 import android.util.Log import android.view.KeyEvent import android.widget.ImageView import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog Loading @@ -49,7 +46,6 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.settings.SettingsFragment import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Type Loading Loading @@ -94,108 +90,6 @@ class MainActivityViewModel @Inject constructor( */ var firstAuthDataFetchTime = 0L /* * Alert dialog to show to user if App Lounge times out. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ private lateinit var timeoutAlertDialog: AlertDialog /** * Display timeout alert dialog. * * @param activity Activity class. Basically the MainActivity. * @param positiveButtonBlock Code block when "Retry" is pressed. * @param openSettings Code block when "Open Settings" button is pressed. * This should open the [SettingsFragment] fragment. * @param applicationTypeFromPreferences Application type string, can be one of * [FusedAPIImpl.APP_TYPE_ANY], [FusedAPIImpl.APP_TYPE_OPEN], [FusedAPIImpl.APP_TYPE_PWA] */ fun displayTimeoutAlertDialog( activity: Activity, positiveButtonBlock: () -> Unit, openSettings: () -> Unit, applicationTypeFromPreferences: String, ) { if (!this::timeoutAlertDialog.isInitialized) { timeoutAlertDialog = AlertDialog.Builder(activity).apply { setTitle(R.string.timeout_title) /* * Prevent dismissing the dialog from pressing outside as it will only * show a blank screen below the dialog. */ setCancelable(false) /* * If user presses back button to close the dialog without selecting anything, * close App Lounge. */ setOnKeyListener { dialog, keyCode, _ -> if (keyCode == KeyEvent.KEYCODE_BACK) { dialog.dismiss() activity.finish() } true } }.create() } timeoutAlertDialog.apply { /* * Set retry button. */ setButton(DialogInterface.BUTTON_POSITIVE, activity.getString(R.string.retry)) { _, _ -> positiveButtonBlock() } /* * Set message based on apps from GPlay of cleanapk. */ setMessage( activity.getString( when (applicationTypeFromPreferences) { FusedAPIImpl.APP_TYPE_ANY -> R.string.timeout_desc_gplay else -> R.string.timeout_desc_cleanapk } ) ) /* * Show "Open Setting" only for GPlay apps. */ if (applicationTypeFromPreferences == FusedAPIImpl.APP_TYPE_ANY) { setButton( DialogInterface.BUTTON_NEUTRAL, activity.getString(R.string.open_settings) ) { _, _ -> openSettings() } } } timeoutAlertDialog.show() } /** * Returns true if [timeoutAlertDialog] is displaying. * Returs false if it is not initialised. */ fun isTimeoutDialogDisplayed(): Boolean { return if (this::timeoutAlertDialog.isInitialized) { timeoutAlertDialog.isShowing } else false } /** * Dismisses the [timeoutAlertDialog] if it is being displayed. * Does nothing if it is not being displayed. * Caller need not check if the dialog is being displayed. */ fun dismissTimeoutDialog() { if (isTimeoutDialogDisplayed()) { try { timeoutAlertDialog.dismiss() } catch (_: Exception) {} } } // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() var installInProgress = false Loading @@ -213,8 +107,10 @@ class MainActivityViewModel @Inject constructor( } fun setFirstTokenFetchTime() { if (firstAuthDataFetchTime == 0L) { firstAuthDataFetchTime = SystemClock.uptimeMillis() } } fun isTimeEligibleForTokenRefresh(): Boolean { return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis Loading @@ -228,6 +124,7 @@ class MainActivityViewModel @Inject constructor( * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ fun retryFetchingTokenAfterTimeout() { firstAuthDataFetchTime = 0 setFirstTokenFetchTime() authValidity.postValue(false) } Loading @@ -248,7 +145,19 @@ class MainActivityViewModel @Inject constructor( if (!authRequestRunning) { authRequestRunning = true viewModelScope.launch { fusedAPIRepository.fetchAuthData() /* * If getting auth data failed, try getting again. * Sending false in authValidity, triggers observer in MainActivity, * causing it to destroy credentials and try to regenerate auth data. * * Issue: * https://gitlab.e.foundation/e/backlog/-/issues/5413 * https://gitlab.e.foundation/e/backlog/-/issues/5404 */ if (!fusedAPIRepository.fetchAuthData()) { authRequestRunning = false authValidity.postValue(false) } } } } Loading
app/src/main/java/foundation/e/apps/api/JobResult.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line package foundation.e.apps.api import foundation.e.apps.utils.enums.ResultStatus /** * Currently defunct, not being used anywhere. * Prototype to merge API request and also get rid of Pair, Triple for timeout related cases. */ open class JobResult<T> private constructor(val status: ResultStatus) { /* * Classes for returning multiple data from a function along with a status * in the form of ResultStatus. * Use the static overloaded create methods (in companion object) to for easy creation. * * If needed to just pass a single data element with status for API requests, * see the static methods success(), error(), loading() (in companion object). */ class of1<A> (val data1: A, status: ResultStatus): JobResult<A>(status) class of2<A,B> (val data1: A, val data2: B, status: ResultStatus): JobResult<A>(status) class of3<A,B,C> (val data1: A, val data2: B, val data3: C, status: ResultStatus): JobResult<A>(status) var message = "" /* * This is the primary data, mainly for API requests which might send null data. * Other data (type B, C ...) are secondary/optional data. * * For non-null return type, directly use of1, of2, of3 ... classes * and directly access data1, data2, data3 ... */ val data: T? get() = when(this) { is of1 -> this.data1 is of2<T, *> -> this.data1 is of3<T, *, *> -> this.data1 else -> null } fun isSuccess(): Boolean { return status == ResultStatus.OK } companion object { fun <A> create(data1: A, status: ResultStatus, message: String? = null): of1<A> { return of1(data1, status).apply { message?.let { this.message = message } } } fun <A,B> create(data1: A, data2: B, status: ResultStatus, message: String? = null): of2<A,B> { return of2(data1, data2, status).apply { message?.let { this.message = message } } } fun <A,B,C> create(data1: A, data2: B, data3: C, status: ResultStatus, message: String? = null): of3<A,B,C> { return of3(data1, data2, data3, status).apply { message?.let { this.message = message } } } /* * Methods for API */ fun <T> success(data: T): JobResult<T> { return of1(data, ResultStatus.OK) } fun <T> error(message: String, data: T? = null): JobResult<T> { val result = if (data == null) JobResult(ResultStatus.UNKNOWN) else of1<T>(data, ResultStatus.UNKNOWN) return result.apply { this.message = message } } /*fun <T> loading(data: T?): JobResult<T> { return if (data == null) JobResult(ResultStatus.LOADING) else JobResult.of1(data, ResultStatus.LOADING) }*/ } } No newline at end of file