diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 98d0c8135..499e4fbbe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,12 @@ + + + + @@ -81,7 +87,8 @@ + android:exported="true" + android:windowSoftInputMode="adjustPan"> diff --git a/app/src/main/java/com/aurora/Constants.kt b/app/src/main/java/com/aurora/Constants.kt index f691fe06a..16732cfe3 100644 --- a/app/src/main/java/com/aurora/Constants.kt +++ b/app/src/main/java/com/aurora/Constants.kt @@ -35,7 +35,8 @@ object Constants { const val SHARE_URL = "https://play.google.com/store/apps/details?id=" const val UPDATE_URL_STABLE = "https://gitlab.com/AuroraOSS/AuroraStore/raw/master/updates.json" - const val UPDATE_URL_NIGHTLY = "https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json" + const val UPDATE_URL_NIGHTLY = + "https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json" const val NOTIFICATION_CHANNEL_EXPORT = "NOTIFICATION_CHANNEL_EXPORT" const val NOTIFICATION_CHANNEL_INSTALL = "NOTIFICATION_CHANNEL_INSTALL" @@ -56,4 +57,6 @@ object Constants { const val PAGE_TYPE = "PAGE_TYPE" const val TOP_CHART_TYPE = "TOP_CHART_TYPE" const val TOP_CHART_CATEGORY = "TOP_CHART_CATEGORY" + + const val JSON_MIME_TYPE = "application/json" } diff --git a/app/src/main/java/com/aurora/extensions/PackageInfo.kt b/app/src/main/java/com/aurora/extensions/PackageInfo.kt index 9d3064488..4a57d3859 100644 --- a/app/src/main/java/com/aurora/extensions/PackageInfo.kt +++ b/app/src/main/java/com/aurora/extensions/PackageInfo.kt @@ -1,14 +1,23 @@ package com.aurora.extensions +import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.os.Build import android.os.Process +import androidx.core.content.pm.PackageInfoCompat fun PackageInfo.isValidApp(packageManager: PackageManager): Boolean { if (this.applicationInfo == null || this.packageName.isEmpty()) return false - // Most core AOSP system apps use their package name as label - if (this.applicationInfo!!.loadLabel(packageManager).startsWith(this.packageName)) return false + // Filter out core AOSP system apps + if (this.applicationInfo!!.flags and ApplicationInfo.FLAG_SYSTEM != 0) { + if (this.packageName.endsWith(".resources")) return false + if (this.applicationInfo!!.loadLabel(packageManager).startsWith(this.packageName)) return false + if (this.versionName?.endsWith("system image") == true) return false + if (this.versionName?.endsWith("-initial") == true) return false + if (this.versionName == Build.VERSION.RELEASE && PackageInfoCompat.getLongVersionCode(this) == Build.VERSION.SDK_INT.toLong()) return false + } return when { isQAndAbove -> { diff --git a/app/src/main/java/com/aurora/store/MainActivity.kt b/app/src/main/java/com/aurora/store/MainActivity.kt index 81e37b6b1..88233a26a 100644 --- a/app/src/main/java/com/aurora/store/MainActivity.kt +++ b/app/src/main/java/com/aurora/store/MainActivity.kt @@ -82,7 +82,7 @@ class MainActivity : AppCompatActivity() { // Adjust root view's paddings for edgeToEdge display ViewCompat.setOnApplyWindowInsetsListener(B.root) { root, windowInsets -> val insets = windowInsets.getInsets(systemBars() or displayCutout() or ime()) - root.setPadding(0, insets.top, 0, 0) + root.setPadding(insets.left, insets.top, insets.right, 0) windowInsets } diff --git a/app/src/main/java/com/aurora/store/data/model/Enumerations.kt b/app/src/main/java/com/aurora/store/data/model/AccountType.kt similarity index 89% rename from app/src/main/java/com/aurora/store/data/model/Enumerations.kt rename to app/src/main/java/com/aurora/store/data/model/AccountType.kt index 9cb35a6c4..a3f59acc4 100644 --- a/app/src/main/java/com/aurora/store/data/model/Enumerations.kt +++ b/app/src/main/java/com/aurora/store/data/model/AccountType.kt @@ -23,12 +23,3 @@ enum class AccountType { ANONYMOUS, GOOGLE } - -enum class State { - IDLE, - QUEUED, - PROGRESS, - COMPLETE, - CANCELED, - INSTALLING, -} diff --git a/app/src/main/java/com/aurora/store/data/model/MinimalApp.kt b/app/src/main/java/com/aurora/store/data/model/MinimalApp.kt index 28d1914c0..a3e0a648c 100644 --- a/app/src/main/java/com/aurora/store/data/model/MinimalApp.kt +++ b/app/src/main/java/com/aurora/store/data/model/MinimalApp.kt @@ -2,34 +2,61 @@ package com.aurora.store.data.model import android.content.Context import android.content.pm.PackageInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.os.Parcelable import androidx.core.content.pm.PackageInfoCompat import com.aurora.gplayapi.data.models.App import com.aurora.store.data.room.update.Update +import com.aurora.store.util.PackageUtil +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class MinimalApp( val packageName: String, + val versionName: String, val versionCode: Int, - val displayName: String + val displayName: String, + @IgnoredOnParcel + val icon: Bitmap? = null ) : Parcelable { companion object { fun fromApp(app: App): MinimalApp { - return MinimalApp(app.packageName, app.versionCode, app.displayName) + return MinimalApp( + app.packageName, + app.versionName, + app.versionCode, + app.displayName + ) + } + + fun toApp(minimalApp: MinimalApp): App { + return App(minimalApp.packageName).apply { + versionName = minimalApp.versionName ?: "" + versionCode = minimalApp.versionCode + displayName = minimalApp.displayName + } } fun fromUpdate(update: Update): MinimalApp { - return MinimalApp(update.packageName, update.versionCode, update.displayName) + return MinimalApp( + update.packageName, + update.versionName, + update.versionCode, + update.displayName + ) } fun fromPackageInfo(context: Context, packageInfo: PackageInfo): MinimalApp { return MinimalApp( packageInfo.packageName, + packageInfo.versionName ?: "", PackageInfoCompat.getLongVersionCode(packageInfo).toInt(), - packageInfo.applicationInfo!!.loadLabel(context.packageManager).toString() + packageInfo.applicationInfo!!.loadLabel(context.packageManager).toString(), + PackageUtil.getIconForPackage(context, packageInfo.packageName) ) } } diff --git a/app/src/main/java/com/aurora/store/data/network/OkHttpClientModule.kt b/app/src/main/java/com/aurora/store/data/network/OkHttpClientModule.kt index a04e93312..8278521d4 100644 --- a/app/src/main/java/com/aurora/store/data/network/OkHttpClientModule.kt +++ b/app/src/main/java/com/aurora/store/data/network/OkHttpClientModule.kt @@ -22,6 +22,7 @@ package com.aurora.store.data.network import android.content.Context import android.util.Base64 import android.util.Log +import com.aurora.store.BuildConfig import com.aurora.store.R import com.aurora.store.data.model.Algorithm import com.aurora.store.data.model.ProxyInfo @@ -60,16 +61,20 @@ object OkHttpClientModule { @Provides @Singleton fun providesOkHttpClientInstance(certPinner: CertificatePinner, proxy: Proxy?): OkHttpClient { - return OkHttpClient().newBuilder() + val okHttpClientBuilder = OkHttpClient().newBuilder() .proxy(proxy) - .certificatePinner(certPinner) .connectTimeout(25, TimeUnit.SECONDS) .readTimeout(25, TimeUnit.SECONDS) .writeTimeout(25, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .followRedirects(true) .followSslRedirects(true) - .build() + + if (!BuildConfig.DEBUG) { + okHttpClientBuilder.certificatePinner(certPinner) + } + + return okHttpClientBuilder.build() } @Provides diff --git a/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt b/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt index 4ab8da49b..b71017988 100644 --- a/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt +++ b/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt @@ -3,6 +3,8 @@ package com.aurora.store.data.room.favourite import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey +import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.data.models.Artwork import kotlinx.parcelize.Parcelize @Parcelize @@ -16,6 +18,26 @@ data class Favourite( val mode: Mode ) : Parcelable { + companion object { + fun fromApp(app: App, mode: Mode): Favourite { + return Favourite( + packageName = app.packageName, + displayName = app.displayName, + iconURL = app.iconArtwork.url, + added = System.currentTimeMillis(), + mode = mode + ) + } + + fun Favourite.toApp(): App { + return App( + packageName = packageName, + displayName = displayName, + iconArtwork = Artwork(url = iconURL) + ) + } + } + enum class Mode { MANUAL, IMPORT diff --git a/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt b/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt index b62cda05d..3ce970815 100644 --- a/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt @@ -1,7 +1,13 @@ package com.aurora.store.data.work +import android.accounts.Account +import android.accounts.AccountManager import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Base64 import android.util.Log +import androidx.core.os.bundleOf import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters @@ -10,8 +16,16 @@ import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.data.model.AccountType import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.runBlocking /** * Worker to refresh [AuthData] in background @@ -26,6 +40,8 @@ open class AuthWorker @AssistedInject constructor( private val TAG = AuthWorker::class.java.simpleName + private val authToken: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + override suspend fun doWork(): Result { if (!AccountProvider.isLoggedIn(appContext)) { Log.i(TAG, "User has logged out!") @@ -42,14 +58,48 @@ open class AuthWorker @AssistedInject constructor( val accountType = AccountProvider.getAccountType(appContext) val authData = when (accountType) { AccountType.GOOGLE -> { - authProvider.buildGoogleAuthData( - AccountProvider.getLoginEmail(appContext)!!, - AccountProvider.getLoginToken(appContext)!!.first, - AccountProvider.getLoginToken(appContext)!!.second - ).getOrThrow() + val email = AccountProvider.getLoginEmail(appContext)!! + val token = AccountProvider.getLoginToken(appContext)!!.first + val tokenType = AccountProvider.getLoginToken(appContext)!!.second + + if (tokenType == AuthHelper.Token.AAS) { + Log.i(TAG, "Refreshing AuthData for personal account") + authProvider.buildGoogleAuthData(email, token, tokenType).getOrThrow() + } else { + /* + * We are working with AuthToken here. The only scenario when we will have + * AuthToken and Google login is when the user used microG to login into + * Aurora Store. In this case, we use system's AccountManager to request credentials. + */ + Log.i(TAG, "Refreshing AuthData for personal account using AccountManager") + AccountManager.get(appContext) + .getAuthToken( + Account(email, GOOGLE_ACCOUNT_TYPE), + GOOGLE_PLAY_AUTH_TOKEN_TYPE, + bundleOf( + "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, + "overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT) + ), + true, + { + authToken.tryEmit(it.result.getString(AccountManager.KEY_AUTHTOKEN)) + }, + Handler(Looper.getMainLooper()) + ) + runBlocking { + authProvider.buildGoogleAuthData( + email, + authToken.take(1).first()!!, + tokenType + ).getOrThrow() + } + } } - AccountType.ANONYMOUS -> authProvider.buildAnonymousAuthData().getOrThrow() + AccountType.ANONYMOUS -> { + Log.i(TAG, "Refreshing AuthData for anonymous account") + authProvider.buildAnonymousAuthData().getOrThrow() + } } require(verifyAndSaveAuth(authData, accountType) != null) diff --git a/app/src/main/java/com/aurora/store/module/HelperModule.kt b/app/src/main/java/com/aurora/store/module/HelperModule.kt index 457afe326..f4d7195fd 100644 --- a/app/src/main/java/com/aurora/store/module/HelperModule.kt +++ b/app/src/main/java/com/aurora/store/module/HelperModule.kt @@ -7,6 +7,7 @@ import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.StreamHelper +import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper import com.aurora.gplayapi.helpers.web.WebCategoryStreamHelper import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper import com.aurora.gplayapi.helpers.web.WebSearchHelper @@ -152,4 +153,15 @@ object HelperModule { .using(httpClient) .with(spoofProvider.locale) } + + @Singleton + @Provides + fun providesWebAppDetailsHelperInstance( + spoofProvider: SpoofProvider, + httpClient: IHttpClient + ): WebAppDetailsHelper { + return WebAppDetailsHelper() + .using(httpClient) + .with(spoofProvider.locale) + } } diff --git a/app/src/main/java/com/aurora/store/util/CertUtil.kt b/app/src/main/java/com/aurora/store/util/CertUtil.kt index aa8d35d9b..309935d5a 100644 --- a/app/src/main/java/com/aurora/store/util/CertUtil.kt +++ b/app/src/main/java/com/aurora/store/util/CertUtil.kt @@ -32,11 +32,27 @@ import com.aurora.store.data.model.Algorithm import com.aurora.store.util.PackageUtil.getPackageInfo import java.security.MessageDigest import java.security.cert.X509Certificate +import javax.security.auth.x500.X500Principal object CertUtil { private val TAG = "CertUtil" + const val GOOGLE_ACCOUNT_TYPE = "com.google" + const val GOOGLE_PLAY_AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay" + const val GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending" + const val GOOGLE_PLAY_CERT = + "MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK" + + // Keep this list updated as & when new signatures are added. + private val knownGMSSignatures = listOf( + "bd32424203e0fb25f36b57e5aa356f9bdd1da998", + "38918a453d07199354f8b19af05ec6562ced5788,", + "2169eddb5fbb1fdf241c262681024692c4fc1ecb", + "58e1c4133f7441ec3d2c270270a14802da47ba0e", + "4f87463a1ae6f7d71b2c0b0658845790236dba42" + ) + fun isFDroidApp(context: Context, packageName: String): Boolean { return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName) } @@ -84,6 +100,31 @@ object CertUtil { } } + fun isGoogleGMS(context: Context, packageName: String): Boolean { + return try { + getX509Certificates(context, packageName).any { certificate -> + val signatureHash = extractSHA1Fingerprint(certificate) + + if (knownGMSSignatures.contains(signatureHash)) return true + + // Follow heuristics to determine if the app is signed by Google, just to ensure we don't miss any. + listOf( + certificate.issuerX500Principal, + certificate.subjectX500Principal + ).any { + val map = parseX500Principal(it) + map["O"] == "Google LLC" || map["O"] == "Google Inc." + && map["L"] == "Mountain View" + && map["ST"] == "California" + && map["C"] == "US" + } + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to check signing cert for $packageName") + false + } + } + private fun isInstalledByFDroid(context: Context, packageName: String): Boolean { val fdroidPackages = listOf( "org.fdroid.basic", "org.fdroid.fdroid", "org.fdroid.fdroid.privileged" @@ -120,4 +161,19 @@ object CertUtil { getPackageInfo(context, packageName, PackageManager.GET_SIGNATURES) } } + + private fun extractSHA1Fingerprint(certificate: X509Certificate): String { + val messageDigest = MessageDigest.getInstance(Algorithm.SHA1.value) + messageDigest.update(certificate.encoded) + return messageDigest.digest() + .joinToString("") { byte -> String.format("%02x", byte) } + .lowercase() + } + + private fun parseX500Principal(principal: X500Principal): Map { + return principal.name.split(",").associate { + val (left, right) = it.split("=") + left.trim() to right.trim() + } + } } diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index 540f06b23..e3491cc3b 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -27,11 +27,13 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.PackageInfoFlags import android.content.pm.SharedLibraryInfo import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.provider.Settings import android.util.Log import androidx.annotation.RequiresApi +import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.pm.PackageInfoCompat import androidx.core.graphics.drawable.toBitmap import com.aurora.extensions.isHuawei @@ -40,12 +42,17 @@ import com.aurora.extensions.isPAndAbove import com.aurora.extensions.isTAndAbove import com.aurora.extensions.isValidApp import com.aurora.store.BuildConfig +import com.aurora.store.R import java.util.Locale object PackageUtil { private const val TAG = "PackageUtil" + private const val PACKAGE_NAME_MICRO_G = "com.google.android.gms" + private const val VERSION_CODE_MICRO_G = 240913402 + private const val VERSION_CODE_MICRO_G_HUAWEI = 240913007 + fun getAllValidPackages(context: Context): List { val sharedLibs = context.packageManager.systemSharedLibraryNames ?: emptyArray() return context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA) @@ -57,6 +64,19 @@ object PackageUtil { } } + fun hasSupportedMicroG(context: Context): Boolean { + val isGoogle = CertUtil.isGoogleGMS(context, PACKAGE_NAME_MICRO_G) + + // Do not check for MicroG if Google Play Services is installed + if (isGoogle) return false + + return if (isHuawei) { + isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G_HUAWEI) + } else { + isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G) + } + } + fun isInstalled(context: Context, packageName: String): Boolean { return try { getPackageInfo(context, packageName, PackageManager.GET_META_DATA) @@ -218,6 +238,20 @@ object PackageUtil { } } + fun getIconDrawableForPackage(context: Context, packageName: String): Drawable? { + val placeholder = AppCompatResources.getDrawable(context, R.drawable.bg_placeholder) + + return try { + val packageInfo = context.packageManager.getPackageInfo(packageName, 0) + val applicationInfo = packageInfo.applicationInfo ?: return placeholder + + applicationInfo.loadIcon(context.packageManager) + } catch (exception: Exception) { + Log.e(TAG, "Failed to get icon for package!", exception) + placeholder + } + } + private fun getAllSharedLibraries(context: Context, flags: Int = 0): List { return if (isTAndAbove) { context.packageManager.getSharedLibraries(PackageInfoFlags.of(flags.toLong())) diff --git a/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt deleted file mode 100644 index a77395be2..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.custom.layouts.button - -import android.content.Context -import android.content.res.ColorStateList -import android.util.AttributeSet -import android.widget.RelativeLayout -import androidx.core.content.ContextCompat -import com.aurora.store.R -import com.aurora.store.data.model.State -import com.aurora.store.databinding.ViewActionButtonBinding - -class ActionButton : RelativeLayout { - - private lateinit var binding: ViewActionButtonBinding - - constructor(context: Context) : super(context) { - init(context, null) - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init(context, attrs) - } - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context, attrs) - } - - private fun init(context: Context, attrs: AttributeSet?) { - val view = inflate(context, R.layout.view_action_button, this) - binding = ViewActionButtonBinding.bind(view) - - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ActionButton) - val btnTxt = typedArray.getString(R.styleable.ActionButton_btnActionText) - - val btnTxtColor = typedArray.getResourceId( - R.styleable.ActionButton_btnActionTextColor, - R.color.colorWhite - ) - - val stateIcon = typedArray.getResourceId( - R.styleable.ActionButton_btnActionIcon, - R.drawable.ic_check - ) - - val stateColor = ContextCompat.getColor(context, btnTxtColor) - - binding.btn.text = btnTxt - binding.btn.setTextColor(stateColor) - binding.img.setImageDrawable(ContextCompat.getDrawable(context, stateIcon)) - binding.img.imageTintList = ColorStateList.valueOf(stateColor) - - typedArray.recycle() - } - - fun setText(text: String) { - binding.viewFlipper.displayedChild = 0 - binding.btn.text = text - } - - fun setText(text: Int) { - binding.viewFlipper.displayedChild = 0 - binding.btn.text = ContextCompat.getString(context, text) - } - - fun setButtonState(enabled: Boolean = true) { - binding.btn.isEnabled = enabled - } - - fun updateState(state: State) { - val displayChild = when (state) { - State.PROGRESS -> 1 - State.COMPLETE -> 2 - else -> 0 - } - - if (binding.viewFlipper.displayedChild != displayChild) { - binding.viewFlipper.displayedChild = displayChild - - if (displayChild == 2) updateState(State.IDLE) - } - } - - fun addOnClickListener(onClickListener: OnClickListener?) { - binding.btn.setOnClickListener(onClickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/PackageInfoView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt similarity index 75% rename from app/src/main/java/com/aurora/store/view/epoxy/views/PackageInfoView.kt rename to app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt index 37a7cd8ba..efaa492c4 100644 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/PackageInfoView.kt +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt @@ -20,9 +20,7 @@ package com.aurora.store.view.epoxy.views import android.content.Context -import android.content.pm.PackageInfo import android.util.AttributeSet -import androidx.core.content.pm.PackageInfoCompat import coil3.load import coil3.request.placeholder import coil3.request.transformations @@ -31,30 +29,29 @@ import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelView import com.aurora.store.R +import com.aurora.store.data.model.MinimalApp import com.aurora.store.databinding.ViewPackageBinding -import com.aurora.store.util.PackageUtil @ModelView( autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, baseModelClass = BaseModel::class ) -class PackageInfoView @JvmOverloads constructor( +class InstalledAppView @JvmOverloads constructor( context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : BaseView(context, attrs, defStyleAttr) { @ModelProp(options = [ModelProp.Option.IgnoreRequireHashCode]) - fun packageInfo(packageInfo: PackageInfo) { - val appInfo = packageInfo.applicationInfo!! - binding.imgIcon.load(PackageUtil.getIconForPackage(context, appInfo.packageName)) { + fun packageInfo(app: MinimalApp) { + binding.imgIcon.load(app.icon) { placeholder(R.drawable.bg_placeholder) transformations(RoundedCornersTransformation(25F)) } - binding.txtLine1.text = appInfo.loadLabel(context.packageManager) - binding.txtLine2.text = appInfo.packageName - binding.txtLine3.text = ("${packageInfo.versionName}.${PackageInfoCompat.getLongVersionCode(packageInfo)}") + binding.txtLine1.text = app.displayName + binding.txtLine2.text = app.packageName + binding.txtLine3.text = ("${app.versionName} (${app.versionCode})") } @CallbackProp diff --git a/app/src/main/java/com/aurora/store/view/ui/about/AboutFragment.kt b/app/src/main/java/com/aurora/store/view/ui/about/AboutFragment.kt index 72b12f4f0..b089165d8 100644 --- a/app/src/main/java/com/aurora/store/view/ui/about/AboutFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/about/AboutFragment.kt @@ -42,10 +42,7 @@ class AboutFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutToolbarAction.txtTitle.text = getString(R.string.title_about) - binding.layoutToolbarAction.imgActionPrimary.setOnClickListener { - findNavController().navigateUp() - } + binding.toolbar.setNavigationOnClickListener { findNavController().navigateUp() } // About Details binding.imgIcon.load(R.mipmap.ic_launcher) diff --git a/app/src/main/java/com/aurora/store/view/ui/account/AccountFragment.kt b/app/src/main/java/com/aurora/store/view/ui/account/AccountFragment.kt index c7308dc62..adca32e45 100644 --- a/app/src/main/java/com/aurora/store/view/ui/account/AccountFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/account/AccountFragment.kt @@ -47,10 +47,7 @@ class AccountFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutToolbarAction.txtTitle.text = getString(R.string.title_account_manager) - binding.layoutToolbarAction.imgActionPrimary.setOnClickListener { - findNavController().navigateUp() - } + binding.toolbar.setNavigationOnClickListener { findNavController().navigateUp() } // Chips view.context.apply { diff --git a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt index f69d46304..1c4cb7bcf 100644 --- a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt @@ -19,36 +19,47 @@ package com.aurora.store.view.ui.all -import android.content.pm.PackageInfo +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.Constants +import com.aurora.extensions.toast +import com.aurora.gplayapi.data.models.App import com.aurora.store.AuroraApp +import com.aurora.store.R import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.model.MinimalApp import com.aurora.store.databinding.FragmentGenericWithSearchBinding import com.aurora.store.view.epoxy.views.HeaderViewModel_ -import com.aurora.store.view.epoxy.views.PackageInfoViewModel_ +import com.aurora.store.view.epoxy.views.app.AppListViewModel_ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.all.InstalledViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import java.util.Calendar @AndroidEntryPoint class AppsGamesFragment : BaseFragment() { private val viewModel: InstalledViewModel by viewModels() + private val startForDocumentExport = + registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) { + if (it != null) exportInstalledApps(it) else toast(R.string.toast_fav_export_failed) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { - viewModel.packages.collect { + viewModel.apps.collect { updateController(it) } } @@ -67,38 +78,47 @@ class AppsGamesFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarNative.apply { - imgActionPrimary.visibility = View.VISIBLE - imgActionSecondary.visibility = View.GONE - - imgActionPrimary.setOnClickListener { findNavController().navigateUp() } - - inputSearch.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (s.isNullOrEmpty()) { - updateController(viewModel.packages.value) - } else { - val filteredPackages = viewModel.packages.value?.filter { - it.applicationInfo!!.loadLabel(requireContext().packageManager) - .contains(s, true) || it.packageName.contains(s, true) - } - updateController(filteredPackages) + binding.toolbar.apply { + inflateMenu(R.menu.menu_import_export) + setNavigationOnClickListener { findNavController().navigateUp() } + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_export -> { + startForDocumentExport.launch( + "aurora_store_apps_${Calendar.getInstance().time.time}.json" + ) + true } - } - override fun afterTextChanged(s: Editable?) {} - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { + else -> false } - }) + } } + + binding.searchBar.addTextChangedListener(object : TextWatcher { + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s.isNullOrEmpty()) { + updateController(viewModel.apps.value) + } else { + val filteredPackages = viewModel.apps.value?.filter { + it.displayName.contains(s, true) || it.packageName.contains(s, true) + } + updateController(filteredPackages) + } + } + + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + }) } - private fun updateController(packages: List?) { + private fun updateController(packages: List?) { binding.recycler.withModels { setFilterDuplicates(true) if (packages == null) { @@ -116,12 +136,21 @@ class AppsGamesFragment : BaseFragment() { ) packages.forEach { app -> add( - PackageInfoViewModel_() + AppListViewModel_() .id(app.packageName.hashCode()) - .packageInfo(app) - .click { _ -> openDetailsFragment(app.packageName) } + .app(app) + .click { _ -> + openDetailsFragment( + app.packageName, + app + ) + } .longClick { _ -> - openAppMenuSheet(MinimalApp.fromPackageInfo(requireContext(), app)) + openAppMenuSheet( + MinimalApp.fromApp( + app + ) + ) false } ) @@ -130,4 +159,8 @@ class AppsGamesFragment : BaseFragment() { } } + private fun exportInstalledApps(uri: Uri) { + viewModel.exportApps(requireContext(), uri) + toast(R.string.toast_fav_export_success) + } } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt index 7a74c5d59..35a251b74 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt @@ -20,14 +20,19 @@ package com.aurora.store.view.ui.commons import android.content.pm.PackageInfo +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.Constants +import com.aurora.extensions.toast import com.aurora.store.AuroraApp +import com.aurora.store.R import com.aurora.store.data.event.BusEvent import com.aurora.store.databinding.FragmentGenericWithSearchBinding import com.aurora.store.view.epoxy.views.BlackListViewModel_ @@ -35,12 +40,22 @@ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ import com.aurora.store.viewmodel.all.BlacklistViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import java.util.Calendar @AndroidEntryPoint class BlacklistFragment : BaseFragment() { private val viewModel: BlacklistViewModel by viewModels() + private val startForDocumentImport = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { + if (it != null) importBlacklist(it) else toast(R.string.toast_black_import_failed) + } + private val startForDocumentExport = + registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) { + if (it != null) exportBlacklist(it) else toast(R.string.toast_black_export_failed) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -51,37 +66,62 @@ class BlacklistFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarNative.apply { - imgActionPrimary.visibility = View.VISIBLE - imgActionSecondary.visibility = View.GONE - - imgActionPrimary.setOnClickListener { + binding.toolbar.apply { + inflateMenu(R.menu.menu_blacklist) + setNavigationOnClickListener { viewModel.blacklistProvider.blacklist = viewModel.selected findNavController().navigateUp() } + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_import -> { + startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE)) + } - inputSearch.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (s.isNullOrEmpty()) { - updateController(viewModel.packages.value) - } else { - val filteredPackages = viewModel.packages.value?.filter { - it.applicationInfo!!.loadLabel(requireContext().packageManager) - .contains(s, true) || it.packageName.contains(s, true) - } - updateController(filteredPackages) + R.id.action_export -> { + startForDocumentExport.launch( + "aurora_store_apps_${Calendar.getInstance().time.time}.json" + ) + } + + R.id.action_select_all -> { + viewModel.selectAll() + binding.recycler.requestModelBuild() + true + } + + R.id.action_remove_all -> { + viewModel.removeAll() + binding.recycler.requestModelBuild() + true } } - - override fun afterTextChanged(s: Editable?) {} - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) {} - }) + true + } } + + binding.searchBar.addTextChangedListener(object : TextWatcher { + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s.isNullOrEmpty()) { + updateController(viewModel.packages.value) + } else { + val filteredPackages = viewModel.packages.value?.filter { + it.applicationInfo!!.loadLabel(requireContext().packageManager) + .contains(s, true) || it.packageName.contains(s, true) + } + updateController(filteredPackages) + } + } + + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + }) } override fun onPause() { @@ -125,4 +165,15 @@ class BlacklistFragment : BaseFragment() { } } } + + private fun importBlacklist(uri: Uri) { + viewModel.importBlacklist(requireContext(), uri) + binding.recycler.requestModelBuild() + toast(R.string.toast_black_import_success) + } + + private fun exportBlacklist(uri: Uri) { + viewModel.exportBlacklist(requireContext(), uri) + toast(R.string.toast_black_export_success) + } } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt index bb466bd7c..7fc2cb8e4 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt @@ -21,7 +21,6 @@ package com.aurora.store.view.ui.commons import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -30,7 +29,6 @@ import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.helpers.contracts.StreamContract import com.aurora.gplayapi.utils.CategoryUtil -import com.aurora.store.R import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.databinding.FragmentGenericWithToolbarBinding @@ -57,9 +55,8 @@ class CategoryBrowseFragment : BaseFragment() val genericCarouselController = CategoryCarouselController(this) // Toolbar - binding.layoutToolbarNative.toolbar.apply { + binding.toolbar.apply { title = args.title - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) setNavigationOnClickListener { findNavController().navigateUp() } } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt index 2841e30d3..c1be4599d 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt @@ -21,13 +21,11 @@ package com.aurora.store.view.ui.commons import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.airbnb.epoxy.EpoxyModel import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.store.R import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_ @@ -51,9 +49,8 @@ class ExpandedStreamBrowseFragment : BaseFragment() { private val viewModel: FavouriteViewModel by viewModels() - private val mimeType = "application/json" private val startForDocumentImport = registerForActivityResult(ActivityResultContracts.OpenDocument()) { - if (it != null) importDeviceConfig(it) else toast(R.string.toast_fav_import_failed) + if (it != null) importFavourites(it) else toast(R.string.toast_fav_import_failed) } private val startForDocumentExport = - registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { - if (it != null) exportDeviceConfig(it) else toast(R.string.toast_fav_export_failed) + registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) { + if (it != null) exportFavourites(it) else toast(R.string.toast_fav_export_failed) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -65,7 +66,7 @@ class FavouriteFragment : BaseFragment() { binding.toolbar.apply { setOnMenuItemClickListener { when (it.itemId) { - R.id.action_import -> startForDocumentImport.launch(arrayOf(mimeType)) + R.id.action_import -> startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE)) R.id.action_export -> { startForDocumentExport.launch( "aurora_store_favourites_${Calendar.getInstance().time.time}.json" @@ -103,7 +104,7 @@ class FavouriteFragment : BaseFragment() { FavouriteViewModel_() .id(it.packageName.hashCode()) .favourite(it) - .onClick { _ -> openDetailsFragment(it.packageName) } + .onClick { _ -> openDetailsFragment(it.packageName, it.toApp()) } .onFavourite { _ -> viewModel.removeFavourite(it.packageName) } ) } @@ -111,13 +112,13 @@ class FavouriteFragment : BaseFragment() { } } - private fun importDeviceConfig(uri: Uri) { + private fun importFavourites(uri: Uri) { viewModel.importFavourites(requireContext(), uri) binding.recycler.requestModelBuild() toast(R.string.toast_fav_import_success) } - private fun exportDeviceConfig(uri: Uri) { + private fun exportFavourites(uri: Uri) { viewModel.exportFavourites(requireContext(), uri) toast(R.string.toast_fav_export_success) } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/StreamBrowseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/StreamBrowseFragment.kt index 397f6d52f..2c0f0c8f6 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/StreamBrowseFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/StreamBrowseFragment.kt @@ -21,12 +21,10 @@ package com.aurora.store.view.ui.commons import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.store.R import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.epoxy.views.AppProgressViewModel_ @@ -48,9 +46,8 @@ class StreamBrowseFragment : BaseFragment() { streamCluster = args.cluster // Toolbar - binding.layoutToolbarNative.toolbar.apply { + binding.toolbar.apply { title = streamCluster.clusterTitle - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) setNavigationOnClickListener { findNavController().navigateUp() } } diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt index 7a2b96c83..3453781f1 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt @@ -20,33 +20,35 @@ package com.aurora.store.view.ui.details +import android.animation.ObjectAnimator import android.content.ActivityNotFoundException import android.content.Intent +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle import android.provider.Settings import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout +import android.view.animation.AccelerateDecelerateInterpolator import android.widget.RelativeLayout import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import coil3.asDrawable import coil3.load -import coil3.request.placeholder +import coil3.request.error import coil3.request.transformations +import coil3.transform.CircleCropTransformation import coil3.transform.RoundedCornersTransformation import com.aurora.Constants import com.aurora.Constants.EXODUS_SUBMIT_PAGE import com.aurora.extensions.browse import com.aurora.extensions.hide +import com.aurora.extensions.px import com.aurora.extensions.requiresObbDir import com.aurora.extensions.runOnUiThread import com.aurora.extensions.share @@ -66,7 +68,6 @@ import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.PermissionType -import com.aurora.store.data.model.State import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.data.providers.AuthProvider @@ -75,6 +76,8 @@ import com.aurora.store.util.CertUtil import com.aurora.store.util.CommonUtil import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR +import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED import com.aurora.store.util.ShortcutManagerUtil import com.aurora.store.view.custom.RatingView import com.aurora.store.view.epoxy.controller.DetailsCarouselController @@ -85,11 +88,10 @@ import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.details.AppDetailsViewModel import com.aurora.store.viewmodel.details.DetailsClusterViewModel -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject @@ -106,46 +108,35 @@ class AppDetailsFragment : BaseFragment() { @Inject lateinit var authProvider: AuthProvider - private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var app: App + private lateinit var iconDrawable: Drawable private var streamBundle: StreamBundle? = StreamBundle() private val isExternal get() = activity?.intent?.action != Intent.ACTION_MAIN - private var downloadStatus = DownloadStatus.UNAVAILABLE - private var isUpdatable: Boolean = false - private var uninstallActionEnabled = false + private val isExtendedUpdateEnabled: Boolean + get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED) + private val showSimilarApps: Boolean + get() = Preferences.getBoolean(requireContext(), PREFERENCE_SIMILAR) private fun onEvent(event: Event) { when (event) { is InstallerEvent.Installed -> { if (app.packageName == event.packageName) { - attachActions() - binding.layoutDetailsToolbar.toolbar.menu.apply { - findItem(R.id.action_home_screen)?.isVisible = - ShortcutManagerUtil.canPinShortcut(requireContext(), app.packageName) - findItem(R.id.action_uninstall)?.isVisible = true - findItem(R.id.menu_app_settings)?.isVisible = true - } + checkAndSetupInstall() } } is InstallerEvent.Uninstalled -> { if (app.packageName == event.packageName) { - attachActions() - binding.layoutDetailsToolbar.toolbar.menu.apply { - findItem(R.id.action_home_screen)?.isVisible = false - findItem(R.id.action_uninstall)?.isVisible = false - findItem(R.id.menu_app_settings)?.isVisible = false - } + checkAndSetupInstall() } } is BusEvent.ManualDownload -> { if (app.packageName == event.packageName) { - app.versionCode = event.versionCode - purchase() + purchase(app.copy(versionCode = event.versionCode)) } } @@ -164,8 +155,7 @@ class AppDetailsFragment : BaseFragment() { is InstallerEvent.Installing -> { if (event.packageName == app.packageName) { - attachActions() - updateActionState(State.INSTALLING) + checkAndSetupInstall() } } @@ -178,35 +168,17 @@ class AppDetailsFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Adjust margins for edgeToEdge display - ViewCompat.setOnApplyWindowInsetsListener(binding.layoutDetailsDev.root) { v, w -> - val insets = w.getInsets(WindowInsetsCompat.Type.navigationBars()) - v.updateLayoutParams { - bottomMargin += insets.bottom - } - WindowInsetsCompat.CONSUMED + app = args.app ?: App(args.packageName) + app.apply { + // Check whether app is installed or not + isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) } - ViewCompat.setOnApplyWindowInsetsListener(binding.layoutDetailsInstall.viewFlipper) { v, w -> - val insets = w.getInsets(WindowInsetsCompat.Type.navigationBars()) - v.updateLayoutParams { - bottomMargin += insets.bottom - } - WindowInsetsCompat.CONSUMED - } - - if (args.app != null) { - app = args.app!! - inflatePartialApp() - } else { - app = App(args.packageName) - } + // Show the basic app details, while the rest of the data is being fetched + updateAppHeader(app, false) // Toolbar - attachToolbar() - - // Check whether app is installed or not - app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) + updateToolbar(app) // App Details viewModel.fetchAppDetails(app.packageName) @@ -215,38 +187,55 @@ class AppDetailsFragment : BaseFragment() { viewModel.app.collect { if (it.packageName.isNotBlank()) { app = it - inflatePartialApp() // Re-inflate the app details, as web data may vary. - inflateExtraDetails(app) + + // App User Review + // We can not fetch it outside of this block, as we need the testing program status + if (!authProvider.isAnonymous && app.isInstalled) { + viewModel.fetchUserAppReview(app) + } + + updateToolbar(app) + updateAppHeader(app) // Re-inflate the app details, as web data may vary. + updateExtraDetails(app) + + if (app.versionCode == 0) { + warnAppUnavailable(app) + } + + // Fetch App Reviews viewModel.fetchAppReviews(app.packageName) + + // Fetch Data Safety Report + viewModel.fetchAppDataSafetyReport(app.packageName) + + // Fetch Exodus Privacy Report + viewModel.fetchAppReport(app.packageName) } else { - toast("Failed to fetch app details") + toast(getString(R.string.status_unavailable)) + // TODO: Redirect to App Unavailable Fragment } } } - viewLifecycleOwner.lifecycleScope.launch { - viewModel.downloadsList - .filter { list -> list.any { it.packageName == app.packageName } } - .collectLatest { downloadsList -> - val download = downloadsList.find { it.packageName == app.packageName } - download?.let { - downloadStatus = it.downloadStatus - - if (it.isFinished) flip(0) else flip(1) - when (it.downloadStatus) { - DownloadStatus.QUEUED -> { - updateProgress(it.progress) - } - - DownloadStatus.DOWNLOADING -> { - updateProgress(it.progress, it.speed, it.timeRemaining) - } - - else -> {} - } + viewModel.download.filterNotNull().onEach { + when (it.downloadStatus) { + DownloadStatus.QUEUED, + DownloadStatus.DOWNLOADING -> { + updateProgress(it.progress) + binding.layoutDetailsApp.btnPrimaryAction.apply { + isEnabled = false + text = getString(R.string.action_open) + setOnClickListener { openApp() } + } + binding.layoutDetailsApp.btnSecondaryAction.apply { + text = getString(R.string.action_cancel) + setOnClickListener { viewModel.cancelDownload(app) } } } - } + + else -> checkAndSetupInstall() + } + }.launchIn(viewLifecycleOwner.lifecycleScope) // Reviews viewLifecycleOwner.lifecycleScope.launch { @@ -261,12 +250,7 @@ class AppDetailsFragment : BaseFragment() { viewLifecycleOwner.lifecycleScope.launch { viewModel.userReview.collect { if (it.commentId.isNotEmpty()) { - binding.layoutDetailsReview.userStars.rating = it.rating.toFloat() - Toast.makeText( - requireContext(), - getString(R.string.toast_rated_success), - Toast.LENGTH_SHORT - ).show() + runOnUiThread { updateUserReview(it) } } else { Toast.makeText( requireContext(), @@ -350,12 +334,10 @@ class AppDetailsFragment : BaseFragment() { viewLifecycleOwner.lifecycleScope.launch { viewModel.favourite.collect { if (it) { - binding.layoutDetailsToolbar.toolbar.menu - ?.findItem(R.id.action_favourite) + binding.toolbar.menu?.findItem(R.id.action_favourite) ?.setIcon(R.drawable.ic_favorite_checked) } else { - binding.layoutDetailsToolbar.toolbar.menu - ?.findItem(R.id.action_favourite) + binding.toolbar.menu?.findItem(R.id.action_favourite) ?.setIcon(R.drawable.ic_favorite_unchecked) } } @@ -368,12 +350,6 @@ class AppDetailsFragment : BaseFragment() { } } - binding.layoutDetailsInstall.progressDownload.clipToOutline = true - binding.layoutDetailsInstall.imgCancel.setOnClickListener { - viewModel.cancelDownload(app) - if (downloadStatus != DownloadStatus.DOWNLOADING) flip(0) - } - viewLifecycleOwner.lifecycleScope.launch { AuroraApp.events.busEvent.collect { onEvent(it) } } @@ -387,15 +363,11 @@ class AppDetailsFragment : BaseFragment() { super.onResume() } - private fun attachActions() { - flip(0) - checkAndSetupInstall() - } - - private fun attachToolbar() { - binding.layoutDetailsToolbar.toolbar.apply { + private fun updateToolbar(app: App) { + binding.toolbar.apply { elevation = 0f navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_arrow_back) + setNavigationOnClickListener { if (isExternal) { activity?.finish() @@ -404,8 +376,23 @@ class AppDetailsFragment : BaseFragment() { } } - inflateMenu(R.menu.menu_details) + if (menu.size() == 0) { + // Inflate Menu only if it is not already inflated + inflateMenu(R.menu.menu_details) + } + // Adjust Menu Items + menu.let { + it.findItem(R.id.action_home_screen)?.isVisible = + app.isInstalled && ShortcutManagerUtil.canPinShortcut( + requireContext(), + app.packageName + ) + it.findItem(R.id.action_uninstall)?.isVisible = app.isInstalled + it.findItem(R.id.menu_app_settings)?.isVisible = app.isInstalled + } + + // Set Menu Item Clicks setOnMenuItemClickListener { when (it.itemId) { R.id.action_home_screen -> { @@ -451,88 +438,56 @@ class AppDetailsFragment : BaseFragment() { requireContext().browse("${Constants.SHARE_URL}${app.packageName}") } } + true } - - if (::app.isInitialized) { - app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) - - menu?.findItem(R.id.action_home_screen)?.isVisible = - app.isInstalled && ShortcutManagerUtil.canPinShortcut( - requireContext(), - app.packageName - ) - - menu?.findItem(R.id.action_uninstall)?.isVisible = app.isInstalled - menu?.findItem(R.id.menu_app_settings)?.isVisible = app.isInstalled - uninstallActionEnabled = app.isInstalled - } } } - private fun attachHeader() { + private fun updateAppHeader(app: App, isFullApp: Boolean = true) { binding.layoutDetailsApp.apply { + val fallbackDrawable = if (app.iconArtwork.url.isNotBlank()) + ContextCompat.getDrawable(requireContext(), R.drawable.bg_placeholder) + else + PackageUtil.getIconDrawableForPackage(requireContext(), app.packageName) + imgIcon.load(app.iconArtwork.url) { - placeholder(R.drawable.bg_placeholder) + error(fallbackDrawable) transformations(RoundedCornersTransformation(32F)) + listener { _, result -> + result.image.asDrawable(resources).let { iconDrawable = it } + } } + packageName.text = app.packageName txtLine1.text = app.displayName txtLine2.text = app.developerName + txtLine3.text = ("${app.versionName} (${app.versionCode})") + txtLine2.setOnClickListener { findNavController().navigate( AppDetailsFragmentDirections .actionAppDetailsFragmentToDevAppsFragment(app.developerName) ) } - txtLine3.text = ("${app.versionName} (${app.versionCode})") - packageName.text = app.packageName - val tags = mutableListOf() - if (app.isFree) - tags.add(getString(R.string.details_free)) - else - tags.add(getString(R.string.details_paid)) + // Do not show tags for web apps or unknown apps + if (isFullApp) { + val tags = mutableSetOf().apply { + if (app.isFree) { + add(getString(R.string.details_free)) + } else { + add(getString(R.string.details_paid)) + } - if (app.containsAds) - tags.add(getString(R.string.details_contains_ads)) - else - tags.add(getString(R.string.details_no_ads)) - - txtLine4.text = tags.joinToString(separator = " • ") - } - } - - private fun attachBottomSheet() { - binding.layoutDetailsInstall.apply { - viewFlipper.setInAnimation(requireContext(), R.anim.fade_in) - viewFlipper.setOutAnimation(requireContext(), R.anim.fade_out) - } - - bottomSheetBehavior = BottomSheetBehavior.from(binding.layoutDetailsInstall.bottomSheet) - bottomSheetBehavior.isDraggable = false - - bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetCallback() { - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_EXPANDED) { - bottomSheetBehavior.setDraggable(true) - } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) { - bottomSheetBehavior.isDraggable = false + if (app.containsAds) { + add(getString(R.string.details_contains_ads)) + } else { + add(getString(R.string.details_no_ads)) + } } - } - override fun onSlide(bottomSheet: View, slideOffset: Float) {} - }) - } - - private fun updateActionState(state: State) { - runOnUiThread { - binding.layoutDetailsInstall.btnDownload.apply { - updateState(state) - if (state == State.INSTALLING) { - setButtonState(false) - setText(R.string.action_installing) - } + txtLine4.text = tags.joinToString(separator = " • ") } } } @@ -548,32 +503,15 @@ class AppDetailsFragment : BaseFragment() { } } - @Synchronized - private fun startDownload() { - when (downloadStatus) { - DownloadStatus.DOWNLOADING -> { - flip(1) - toast("Already downloading") - } - - else -> { - flip(1) - purchase() - } - } - } - - private fun purchase() { - bottomSheetBehavior.isHideable = false - bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - updateActionState(State.PROGRESS) - + private fun purchase(app: App) { if (app.fileList.requiresObbDir()) { if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) { viewModel.download(app) } else { permissionProvider.request(PermissionType.STORAGE_MANAGER) { - if (it) viewModel.download(app) else flip(0) + if (it) viewModel.download(app) else { + // TODO: Ask for permission again or redirect to Permission Manager + } } } } else { @@ -581,149 +519,185 @@ class AppDetailsFragment : BaseFragment() { } } - private fun updateProgress(progress: Int, speed: Long = -1, timeRemaining: Long = -1) { - runOnUiThread { - if (progress == 100) { - binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) - return@runOnUiThread - } + private fun updateProgress(progress: Int) { + // No need to update progress if it is already 100% / completed + transformIcon(progress != 100) + if (progress == 100) return - binding.layoutDetailsInstall.apply { - txtProgressPercent.text = ("${progress}%") - progressDownload.apply { - this.progress = progress - isIndeterminate = progress < 1 - } - txtEta.text = CommonUtil.getETAString(requireContext(), timeRemaining) - txtSpeed.text = CommonUtil.getDownloadSpeedString(requireContext(), speed) + binding.layoutDetailsApp.apply { + progressDownload.progress = progress + progressDownload.isIndeterminate = progress < 1 + } + } + + private fun transformIcon(ongoing: Boolean = false) { + if (::iconDrawable.isInitialized.not()) return + + val imgIcon = binding.layoutDetailsApp.imgIcon + val progressDownload = binding.layoutDetailsApp.progressDownload + + // Avoids flickering when the download is in progress + if (progressDownload.isShown && ongoing) return + if (!progressDownload.isShown && !ongoing) return + + binding.layoutDetailsApp.progressDownload.isVisible = ongoing + + val scaleFactor = if (ongoing) 0.75f else 1f + val scale = listOf( + ObjectAnimator.ofFloat(imgIcon, "scaleX", scaleFactor), + ObjectAnimator.ofFloat(imgIcon, "scaleY", scaleFactor) + ) + + scale.forEach { animation -> + animation.apply { + interpolator = AccelerateDecelerateInterpolator() + duration = 250 + start() } } + + imgIcon.load(iconDrawable) { + transformations( + if (ongoing) { + CircleCropTransformation() + } else { + RoundedCornersTransformation(8.px.toFloat()) + } + ) + } } private fun checkAndSetupInstall() { app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) - runOnUiThread { - binding.layoutDetailsInstall.btnDownload.let { btn -> - btn.setButtonState(true) - if (app.isInstalled) { - val isExtendedUpdateEnabled = Preferences.getBoolean( - requireContext(), Preferences.PREFERENCE_UPDATES_EXTENDED - ) - val needsExtendedUpdate = !app.certificateSetList.any { - it.certificateSet in CertUtil.getEncodedCertificateHashes( - requireContext(), app.packageName - ) - } - isUpdatable = PackageUtil.isUpdatable( - requireContext(), - app.packageName, - app.versionCode.toLong() - ) + // Setup primary and secondary action buttons + binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true + binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true - val installedVersion = - PackageUtil.getInstalledVersion(requireContext(), app.packageName) + if (app.isInstalled) { + val isUpdatable = PackageUtil.isUpdatable(requireContext(), app.packageName, app.versionCode.toLong()) + val hasValidCert = app.certificateSetList.any { + it.certificateSet in CertUtil.getEncodedCertificateHashes(requireContext(), app.packageName) + } - if (isUpdatable && !needsExtendedUpdate || isUpdatable && isExtendedUpdateEnabled) { - binding.layoutDetailsApp.txtLine3.text = - ("$installedVersion ➔ ${app.versionName} (${app.versionCode})") - btn.setText(R.string.action_update) - btn.addOnClickListener { - if (app.versionCode == 0) { - toast(R.string.toast_app_unavailable) - } else { - startDownload() - } + if ((isUpdatable && hasValidCert) || (isUpdatable && isExtendedUpdateEnabled)) { + binding.layoutDetailsApp.btnPrimaryAction.apply { + text = getString(R.string.action_update) + setOnClickListener { + if (app.versionCode == 0) { + toast(R.string.toast_app_unavailable) + setText(R.string.status_unavailable) + } else { + purchase(app) } - } else { - binding.layoutDetailsApp.txtLine3.text = installedVersion - btn.setText(R.string.action_open) - btn.addOnClickListener { openApp() } } - if (!uninstallActionEnabled) { - binding.layoutDetailsToolbar.toolbar.invalidateMenu() + } + } else { + binding.layoutDetailsApp.apply { + txtLine3.text = PackageUtil.getInstalledVersion(requireContext(), app.packageName) + btnPrimaryAction.apply { + setText(R.string.action_open) + setOnClickListener { openApp() } + } + } + } + + binding.layoutDetailsApp.btnSecondaryAction.apply { + text = getString(R.string.action_uninstall) + setOnClickListener { + AppInstaller.uninstall(requireContext(), app.packageName) + } + } + } else { + if (app.isFree) { + binding.layoutDetailsApp.btnPrimaryAction.setText(R.string.action_install) + } else { + binding.layoutDetailsApp.btnPrimaryAction.text = app.price + } + + binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener { + if (authProvider.isAnonymous && !app.isFree) { + toast(R.string.toast_purchase_blocked) + return@setOnClickListener + } else if (app.versionCode == 0) { + toast(R.string.toast_app_unavailable) + return@setOnClickListener + } + + if (!permissionProvider.isGranted(PermissionType.INSTALL_UNKNOWN_APPS)) { + permissionProvider.request(PermissionType.INSTALL_UNKNOWN_APPS) { + if (it) { + purchase(app) + } else { + toast(R.string.permissions_denied) + } } } else { - if (downloadStatus in DownloadStatus.running) { - flip(1) - } else if (app.isFree) { - btn.setText(R.string.action_install) - } else { - btn.setText(app.price) - } + purchase(app) + } + } - btn.addOnClickListener { - if (!permissionProvider.isGranted(PermissionType.INSTALL_UNKNOWN_APPS)) { - permissionProvider.request(PermissionType.INSTALL_UNKNOWN_APPS) { - if (it) { - btn.setText(R.string.download_metadata) - startDownload() - } - } - } else if (authProvider.isAnonymous && !app.isFree) { - toast(R.string.toast_purchase_blocked) - } else if (app.versionCode == 0) { - toast(R.string.toast_app_unavailable) - } else { - btn.setText(R.string.download_metadata) - startDownload() - } - } - - if (uninstallActionEnabled) { - binding.layoutDetailsToolbar.toolbar.invalidateMenu() - } + binding.layoutDetailsApp.btnSecondaryAction.apply { + text = getString(R.string.title_manual_download) + setOnClickListener { + findNavController().navigate( + AppDetailsFragmentDirections + .actionAppDetailsFragmentToManualDownloadSheet(app) + ) } } } - } - @Synchronized - private fun flip(nextView: Int) { - runOnUiThread { - val displayChild = binding.layoutDetailsInstall.viewFlipper.displayedChild - if (displayChild != nextView) { - binding.layoutDetailsInstall.viewFlipper.displayedChild = nextView - if (nextView == 0) checkAndSetupInstall() + // Lay out the toolbar again + binding.toolbar.invalidateMenu() + + if (app.isInstalled) { + binding.toolbar.menu.apply { + findItem(R.id.action_home_screen)?.isVisible = + ShortcutManagerUtil.canPinShortcut(requireContext(), app.packageName) + findItem(R.id.action_uninstall)?.isVisible = true + findItem(R.id.menu_app_settings)?.isVisible = true + } + } else { + binding.toolbar.menu.apply { + findItem(R.id.action_home_screen)?.isVisible = false + findItem(R.id.action_uninstall)?.isVisible = false + findItem(R.id.menu_app_settings)?.isVisible = false } } + + // Restore icon and progress + updateProgress(100) } - private fun inflatePartialApp() { - if (::app.isInitialized) { - attachHeader() - attachBottomSheet() - attachActions() - } - } + private fun updateExtraDetails(app: App) { + binding.viewFlipper.displayedChild = 1 - private fun inflateExtraDetails(app: App?) { - app?.let { - binding.viewFlipper.displayedChild = 1 - inflateAppDescription(app) - inflateAppRatingAndReviews(app) - inflateAppDevInfo(app) - inflateAppDataSafety(app) - inflateAppPrivacy(app) - inflateAppPermission(app) + updateAppDescription(app) + updateAppRatingAndReviews(app) + updateAppDevInfo(app) + updateAppPermission(app) - if (!authProvider.isAnonymous) { - app.testingProgram?.let { - if (it.isAvailable && it.isSubscribed) { - binding.layoutDetailsApp.txtLine1.text = it.displayName - } + // Allow users to handle beta subscriptions, if logged in by own account. + if (!authProvider.isAnonymous) { + // Update app name to the testing program name, if subscribed + app.testingProgram?.let { + if (it.isAvailable && it.isSubscribed) { + binding.layoutDetailsApp.txtLine1.text = it.displayName } - - inflateBetaSubscription(app) } - if (Preferences.getBoolean(requireContext(), Preferences.PREFERENCE_SIMILAR)) { - inflateAppStream(app) - } + updateBetaSubscription(app) } + + if (showSimilarApps) { + updateAppStream(app) + } + + checkAndSetupInstall() } - private fun inflateAppDescription(app: App) { + private fun updateAppDescription(app: App) { binding.layoutDetailDescription.apply { val installs = CommonUtil.addDiPrefix(app.installs) @@ -780,10 +754,16 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflateAppRatingAndReviews(app: App) { + private fun updateAppRatingAndReviews(app: App) { binding.layoutDetailsReview.apply { - averageRating.text = app.rating.average.toString() - txtReviewCount.text = app.rating.abbreviatedLabel + headerRatingReviews.addClickListener { + findNavController().navigate( + AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsReviewFragment( + app.displayName, + app.packageName + ) + ) + } var totalStars = 0L totalStars += app.rating.oneStar @@ -803,42 +783,34 @@ class AppDetailsFragment : BaseFragment() { averageRating.text = String.format(Locale.getDefault(), "%.1f", app.rating.average) txtReviewCount.text = app.rating.abbreviatedLabel + } + } - layoutUserReview.visibility = - if (authProvider.isAnonymous) View.GONE else View.VISIBLE + private fun updateUserReview(review: Review) { + binding.layoutDetailsReview.apply { + layoutUserReview.visibility = View.VISIBLE + inputTitle.setText(review.title) + inputReview.setText(review.comment) + userStars.rating = review.rating.toFloat() - btnPostReview.setOnClickListener { - if (authProvider.isAnonymous) { - toast(R.string.toast_anonymous_restriction) - } else { - addOrUpdateReview(app, Review().apply { - title = inputTitle.text.toString() - rating = userStars.rating.toInt() - comment = inputReview.text.toString() - }) - } - } - - headerRatingReviews.addClickListener { - findNavController().navigate( - AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsReviewFragment( - app.displayName, - app.packageName + if (!authProvider.isAnonymous && app.isInstalled) { + btnPostReview.setOnClickListener { + addOrUpdateReview( + app, + Review().apply { + title = inputTitle.text.toString() + rating = userStars.rating.toInt() + comment = inputReview.text.toString() + } ) - ) + } + } else { + layoutUserReview.visibility = View.GONE } } } - private fun inflateAppDataSafety(app: App) { - viewModel.fetchAppDataSafetyReport(app.packageName) - } - - private fun inflateAppPrivacy(app: App) { - viewModel.fetchAppReport(app.packageName) - } - - private fun inflateAppDevInfo(app: App) { + private fun updateAppDevInfo(app: App) { binding.layoutDetailsDev.apply { if (app.developerAddress.isNotEmpty()) { devAddress.apply { @@ -868,7 +840,7 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflateBetaSubscription(app: App) { + private fun updateBetaSubscription(app: App) { binding.layoutDetailsBeta.apply { app.testingProgram?.let { betaProgram -> if (betaProgram.isAvailable) { @@ -899,7 +871,7 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflateAppStream(app: App) { + private fun updateAppStream(app: App) { app.detailsStreamUrl?.let { val carouselController = DetailsCarouselController(object : GenericCarouselController.Callbacks { @@ -946,7 +918,7 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflateAppPermission(app: App) { + private fun updateAppPermission(app: App) { binding.layoutDetailsPermissions.apply { headerPermission.addClickListener { if (app.permissions.isNotEmpty()) { @@ -999,6 +971,13 @@ class AppDetailsFragment : BaseFragment() { } } + private fun warnAppUnavailable(app: App) { + AuroraApp.events.send(InstallerEvent.Failed(app.packageName).apply { + error = getString(R.string.status_unavailable) + extra = getString(R.string.toast_app_unavailable) + }) + } + /* App Review Helpers */ private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout { diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DetailsExodusFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DetailsExodusFragment.kt index 9e4fc6aa1..a3be33f54 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DetailsExodusFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DetailsExodusFragment.kt @@ -21,7 +21,6 @@ package com.aurora.store.view.ui.details import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.aurora.Constants @@ -49,9 +48,8 @@ class DetailsExodusFragment : BaseFragment() super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutToolbarNative.toolbar.apply { + binding.toolbar.apply { title = args.displayName - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) setNavigationOnClickListener { findNavController().navigateUp() } } diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DetailsMoreFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DetailsMoreFragment.kt index 14b6cae3a..b59d350e6 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DetailsMoreFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DetailsMoreFragment.kt @@ -60,9 +60,7 @@ class DetailsMoreFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarActionMore.toolbar.setOnClickListener { - findNavController().navigateUp() - } + binding.toolbar.setOnClickListener { findNavController().navigateUp() } inflateDescription(args.app) inflateFiles(args.app) @@ -95,7 +93,7 @@ class DetailsMoreFragment : BaseFragment() { } private fun inflateDescription(app: App) { - binding.layoutToolbarActionMore.txtTitle.text = app.displayName + binding.toolbar.title = app.displayName binding.txtDescription.text = HtmlCompat.fromHtml( app.description, HtmlCompat.FROM_HTML_MODE_COMPACT diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DetailsReviewFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DetailsReviewFragment.kt index 7313ffb19..613ea9281 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DetailsReviewFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DetailsReviewFragment.kt @@ -58,11 +58,9 @@ class DetailsReviewFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarActionReview.apply { - txtTitle.text = args.displayName - toolbar.setOnClickListener { - findNavController().navigateUp() - } + binding.toolbar.apply { + title = args.displayName + setNavigationOnClickListener { findNavController().navigateUp() } } viewModel.liveData.observe(viewLifecycleOwner) { @@ -91,6 +89,7 @@ class DetailsReviewFragment : BaseFragment() { binding.chipGroup.setOnCheckedStateChangeListener { _, checkedIds -> when (checkedIds[0]) { R.id.filter_review_all -> filter = Review.Filter.ALL + R.id.filter_newest_first -> filter = Review.Filter.NEWEST R.id.filter_review_critical -> filter = Review.Filter.CRITICAL R.id.filter_review_positive -> filter = Review.Filter.POSITIVE R.id.filter_review_five -> filter = Review.Filter.FIVE diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt index eee29f42b..2de10d2c9 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt @@ -21,12 +21,10 @@ package com.aurora.store.view.ui.details import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.aurora.gplayapi.data.models.SearchBundle -import com.aurora.store.R import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.epoxy.views.AppProgressViewModel_ @@ -49,9 +47,8 @@ class DevAppsFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarNative.toolbar.apply { + binding.toolbar.apply { title = args.developerName - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) setNavigationOnClickListener { findNavController().navigateUp() } } diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DevProfileFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DevProfileFragment.kt index 68adef3dd..96bcc60e8 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DevProfileFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DevProfileFragment.kt @@ -50,10 +50,9 @@ class DevProfileFragment : BaseFragment(), val developerCarouselController = DeveloperCarouselController(this) // Toolbar - binding.layoutToolbarAction.apply { - txtTitle.text = - if (args.title.isNullOrBlank()) getString(R.string.details_dev_profile) else args.title - toolbar.setOnClickListener { findNavController().navigateUp() } + binding.toolbar.apply { + title = if (args.title.isNullOrBlank()) getString(R.string.details_dev_profile) else args.title + setNavigationOnClickListener { findNavController().navigateUp() } } // RecyclerView @@ -78,7 +77,7 @@ class DevProfileFragment : BaseFragment(), is ViewState.Success<*> -> { (it.data as DevStream).apply { - binding.layoutToolbarAction.txtTitle.text = title + binding.toolbar.title = title binding.txtDevName.text = title binding.txtDevDescription.text = description binding.imgIcon.load(imgUrl) diff --git a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt index 146242f74..ea292f41e 100644 --- a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt @@ -22,7 +22,6 @@ package com.aurora.store.view.ui.downloads import android.os.Bundle import android.text.format.DateUtils import android.view.View -import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.Constants.GITLAB_URL @@ -54,11 +53,7 @@ class DownloadFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutToolbarAction.toolbar.apply { - elevation = 0f - title = getString(R.string.title_download_manager) - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) - inflateMenu(R.menu.menu_download_main) + binding.toolbar.apply { setNavigationOnClickListener { findNavController().navigateUp() } setOnMenuItemClickListener { when (it.itemId) { diff --git a/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt index f14385df2..599541918 100644 --- a/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt @@ -50,7 +50,6 @@ import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.search.SearchResultViewModel -import com.google.android.material.textfield.TextInputEditText import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -59,8 +58,6 @@ class SearchResultsFragment : BaseFragment(), private val viewModel: SearchResultViewModel by viewModels() - private lateinit var searchView: TextInputEditText - private lateinit var sharedPreferences: SharedPreferences private var query: String? = null @@ -84,20 +81,20 @@ class SearchResultsFragment : BaseFragment(), sharedPreferences.registerOnSharedPreferenceChangeListener(this) // Toolbar - binding.layoutViewToolbar.apply { - searchView = inputSearch - imgActionPrimary.setOnClickListener { + binding.toolbar.apply { + setNavigationOnClickListener { + binding.searchBar.hideKeyboard() findNavController().navigateUp() } - imgActionSecondary.setOnClickListener { - findNavController().navigate(R.id.downloadFragment) - } - clearButton.apply { - visibility = if (query.isNullOrBlank()) View.GONE else View.VISIBLE - setOnClickListener { - searchView.text?.clear() - searchView.showKeyboard() + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_clear -> { + binding.searchBar.text?.clear() + binding.searchBar.showKeyboard() + } + R.id.action_download -> findNavController().navigate(R.id.downloadFragment) } + true } } @@ -186,30 +183,29 @@ class SearchResultsFragment : BaseFragment(), } } } else { - binding.recycler - .withModels { - setFilterDuplicates(true) + binding.recycler.withModels { + setFilterDuplicates(true) - filteredAppList.forEach { app -> - add( - AppListViewModel_() - .id(app.id) - .app(app) - .click(View.OnClickListener { - searchView.hideKeyboard() - openDetailsFragment(app.packageName, app) - }) - ) - } - - if (searchBundle.subBundles.isNotEmpty()) { - add( - AppProgressViewModel_() - .id("progress") - ) - } + filteredAppList.forEach { app -> + add( + AppListViewModel_() + .id(app.id) + .app(app) + .click(View.OnClickListener { + binding.searchBar.hideKeyboard() + openDetailsFragment(app.packageName, app) + }) + ) } + if (searchBundle.subBundles.isNotEmpty()) { + add( + AppProgressViewModel_() + .id("progress") + ) + } + } + binding.recycler.adapter?.let { if (it.itemCount < 10) { viewModel.next(searchBundle.subBundles) @@ -219,28 +215,22 @@ class SearchResultsFragment : BaseFragment(), } private fun attachSearch() { - searchView.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - - } + binding.searchBar.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (s.isNotEmpty()) { - binding.layoutViewToolbar.clearButton.visibility = View.VISIBLE - } else { - binding.layoutViewToolbar.clearButton.visibility = View.GONE - } + binding.toolbar.menu.findItem(R.id.action_clear)?.isVisible = s.isNotBlank() } override fun afterTextChanged(s: Editable) {} }) - searchView.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> + binding.searchBar.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == KeyEvent.ACTION_DOWN || actionId == KeyEvent.KEYCODE_ENTER ) { - query = searchView.text.toString() + query = binding.searchBar.text.toString() query?.let { requireArguments().putString("query", it) queryViewModel(it) @@ -252,8 +242,8 @@ class SearchResultsFragment : BaseFragment(), } private fun updateQuery(query: String) { - searchView.text = Editable.Factory.getInstance().newEditable(query) - searchView.setSelection(query.length) + binding.searchBar.text = Editable.Factory.getInstance().newEditable(query) + binding.searchBar.setSelection(query.length) queryViewModel(query) } @@ -277,7 +267,6 @@ class SearchResultsFragment : BaseFragment(), .toList() } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (key == PREFERENCE_FILTER) query?.let { queryViewModel(it) } } diff --git a/app/src/main/java/com/aurora/store/view/ui/search/SearchSuggestionFragment.kt b/app/src/main/java/com/aurora/store/view/ui/search/SearchSuggestionFragment.kt index 1cc4fb5b1..ebebe7013 100644 --- a/app/src/main/java/com/aurora/store/view/ui/search/SearchSuggestionFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/search/SearchSuggestionFragment.kt @@ -37,7 +37,6 @@ import com.aurora.store.databinding.FragmentSearchSuggestionBinding import com.aurora.store.view.epoxy.views.SearchSuggestionViewModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.search.SearchSuggestionViewModel -import com.google.android.material.textfield.TextInputEditText import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -47,26 +46,21 @@ class SearchSuggestionFragment : BaseFragment() private val viewModel: SearchSuggestionViewModel by viewModels() - private lateinit var searchView: TextInputEditText - - private var query: String = String() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutToolbarSearch.apply { - searchView = inputSearch - imgActionPrimary.setOnClickListener { - searchView.hideKeyboard() + binding.toolbar.apply { + setNavigationOnClickListener { + binding.searchBar.hideKeyboard() findNavController().navigateUp() } - imgActionSecondary.setOnClickListener { - findNavController().navigate(R.id.downloadFragment) - } - clearButton.apply { - visibility = if (query.isBlank()) View.GONE else View.VISIBLE - setOnClickListener { searchView.text?.clear() } + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_clear -> binding.searchBar.text?.clear() + R.id.action_download -> findNavController().navigate(R.id.downloadFragment) + } + true } } @@ -81,9 +75,7 @@ class SearchSuggestionFragment : BaseFragment() override fun onResume() { super.onResume() - if (::searchView.isInitialized) { - searchView.showKeyboard() - } + binding.searchBar.showKeyboard() } private fun updateController(searchSuggestions: List) { @@ -98,7 +90,7 @@ class SearchSuggestionFragment : BaseFragment() updateQuery(it.title) } .click { _ -> - searchView.hideKeyboard() + binding.searchBar.hideKeyboard() search(it.title) } ) @@ -107,32 +99,30 @@ class SearchSuggestionFragment : BaseFragment() } private fun setupSearch() { - searchView.addTextChangedListener(object : TextWatcher { + binding.searchBar.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { if (s.isNotEmpty()) { - query = s.toString() + val query = s.toString() if (query.isNotEmpty()) { viewModel.observeStreamBundles(query) } - binding.layoutToolbarSearch.clearButton.visibility = View.VISIBLE - } else { - binding.layoutToolbarSearch.clearButton.visibility = View.GONE } + binding.toolbar.menu.findItem(R.id.action_clear)?.isVisible = s.isNotBlank() } override fun afterTextChanged(s: Editable) {} }) - searchView.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> + binding.searchBar.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == KeyEvent.ACTION_DOWN || actionId == KeyEvent.KEYCODE_ENTER ) { - query = searchView.text.toString() + val query = binding.searchBar.text.toString() if (query.isNotEmpty()) { - searchView.hideKeyboard() + binding.searchBar.hideKeyboard() search(query) return@setOnEditorActionListener true } @@ -142,8 +132,8 @@ class SearchSuggestionFragment : BaseFragment() } private fun updateQuery(query: String) { - searchView.text = Editable.Factory.getInstance().newEditable(query) - searchView.setSelection(query.length) + binding.searchBar.text = Editable.Factory.getInstance().newEditable(query) + binding.searchBar.setSelection(query.length) } private fun search(query: String) { diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt index 5ed9e279f..c2c3b1f24 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt @@ -19,19 +19,35 @@ package com.aurora.store.view.ui.splash +import android.accounts.Account +import android.accounts.AccountManager import android.content.Intent import android.net.UrlQuerySanitizer import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Base64 +import android.util.Log import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.extensions.hide +import com.aurora.extensions.isMAndAbove +import com.aurora.extensions.runOnUiThread import com.aurora.extensions.show +import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.R import com.aurora.store.data.model.AuthState import com.aurora.store.databinding.FragmentSplashBinding +import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME +import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_INTRO @@ -44,8 +60,20 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class SplashFragment : BaseFragment() { + private val TAG = SplashFragment::class.java.simpleName + private val viewModel: AuthViewModel by activityViewModels() + private val startForAccount = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val accountName = it.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) + if (!accountName.isNullOrBlank()) { + requestAuthTokenForGoogle(accountName) + } else { + runOnUiThread { resetActions() } + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -57,9 +85,7 @@ class SplashFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarAction.toolbar.apply { - elevation = 0f - inflateMenu(R.menu.menu_splash) + binding.toolbar.apply { setOnMenuItemClickListener { when (it.itemId) { R.id.menu_blacklist_manager -> { @@ -158,10 +184,10 @@ class SplashFragment : BaseFragment() { private fun updateActionLayout(isVisible: Boolean) { if (isVisible) { binding.layoutAction.show() - binding.layoutToolbarAction.toolbar.visibility = View.VISIBLE + binding.toolbar.visibility = View.VISIBLE } else { binding.layoutAction.hide() - binding.layoutToolbarAction.toolbar.visibility = View.GONE + binding.toolbar.visibility = View.GONE } } @@ -176,7 +202,29 @@ class SplashFragment : BaseFragment() { binding.btnGoogle.addOnClickListener { if (viewModel.authState.value != AuthState.Fetching) { binding.btnGoogle.updateProgress(true) - findNavController().navigate(R.id.googleFragment) + if (isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext())) { + val accounts = fetchGoogleAccounts() + + // Do not show selection dialog if there is only one account available + if (accounts.isNotEmpty() && accounts.size == 1) { + requestAuthTokenForGoogle(accounts.first().name) + return@addOnClickListener + } + + Log.i(TAG, "Found supported microG, trying to request credentials") + val accountIntent = AccountManager.newChooseAccountIntent( + null, + null, + arrayOf(GOOGLE_ACCOUNT_TYPE), + null, + null, + null, + null + ) + startForAccount.launch(accountIntent) + } else { + findNavController().navigate(R.id.googleFragment) + } } } } @@ -194,7 +242,8 @@ class SplashFragment : BaseFragment() { } private fun navigateToDefaultTab() { - val defaultDestination = Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB) + val defaultDestination = + Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB) val directions = when (requireArguments().getInt("destinationId", defaultDestination)) { R.id.updatesFragment -> { @@ -206,19 +255,49 @@ class SplashFragment : BaseFragment() { 2 -> SplashFragmentDirections.actionSplashFragmentToUpdatesFragment() else -> SplashFragmentDirections.actionSplashFragmentToNavigationApps() } - activity?.viewModelStore?.clear() // Clear ViewModelStore to avoid bugs with logout + requireActivity().viewModelStore.clear() // Clear ViewModelStore to avoid bugs with logout findNavController().navigate(directions) } private fun getPackageName(): String { // Navigation component cannot handle market scheme as its missing a valid host - return if (activity?.intent != null && activity?.intent?.scheme == "market") { + return if (requireActivity().intent != null && requireActivity().intent.scheme == "market") { requireActivity().intent.data!!.getQueryParameter("id") ?: "" - } else if (activity?.intent != null && activity?.intent?.action == Intent.ACTION_SEND) { - val clipData = activity?.intent?.getStringExtra(Intent.EXTRA_TEXT) ?: "" + } else if (requireActivity().intent != null && requireActivity().intent.action == Intent.ACTION_SEND) { + val clipData = requireActivity().intent.getStringExtra(Intent.EXTRA_TEXT) ?: "" UrlQuerySanitizer(clipData).getValue("id") ?: "" } else { requireArguments().getString("packageName") ?: "" } } + + private fun fetchGoogleAccounts(): Array { + val accountManager = AccountManager.get(requireContext()) + return accountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE) + } + + private fun requestAuthTokenForGoogle(accountName: String) { + try { + AccountManager.get(requireContext()) + .getAuthToken( + Account(accountName, GOOGLE_ACCOUNT_TYPE), + GOOGLE_PLAY_AUTH_TOKEN_TYPE, + bundleOf( + "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, + "overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT) + ), + requireActivity(), + { + viewModel.buildGoogleAuthData( + accountName, + it.result.getString(AccountManager.KEY_AUTHTOKEN) ?: "", + AuthHelper.Token.AUTH + ) + }, + Handler(Looper.getMainLooper()) + ) + } catch (exception: Exception) { + Log.e(TAG, "Failed to get authToken for Google login") + } + } } diff --git a/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt b/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt index 16df22231..b7410ad3c 100644 --- a/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt @@ -25,7 +25,6 @@ import android.os.Bundle import android.util.Log import android.view.View import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle @@ -34,7 +33,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import com.aurora.extensions.toast import com.aurora.store.R import com.aurora.store.data.providers.NativeDeviceInfoProvider -import com.aurora.store.databinding.FragmentGenericWithPagerBinding +import com.aurora.store.databinding.FragmentSpoofBinding import com.aurora.store.util.PathUtil import com.aurora.store.view.ui.commons.BaseFragment import com.google.android.material.tabs.TabLayout @@ -42,7 +41,7 @@ import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class SpoofFragment : BaseFragment() { +class SpoofFragment : BaseFragment() { private val TAG = SpoofFragment::class.java.simpleName // Android is weird, even if export device config with proper mime type, it will refuse to open @@ -63,11 +62,7 @@ class SpoofFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutActionToolbar.toolbar.apply { - elevation = 0f - title = getString(R.string.title_spoof_manager) - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) - inflateMenu(R.menu.menu_import_export) + binding.toolbar.apply { setNavigationOnClickListener { findNavController().navigateUp() } setOnMenuItemClickListener { when (it.itemId) { diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt index 6a363afea..acea39901 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt @@ -21,11 +21,14 @@ package com.aurora.store.viewmodel.all import android.content.Context import android.content.pm.PackageInfo +import android.net.Uri import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.store.data.providers.BlacklistProvider import com.aurora.store.util.PackageUtil +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -37,6 +40,7 @@ import javax.inject.Inject @HiltViewModel class BlacklistViewModel @Inject constructor( val blacklistProvider: BlacklistProvider, + val gson: Gson, @ApplicationContext private val context: Context ) : ViewModel() { @@ -60,4 +64,44 @@ class BlacklistViewModel @Inject constructor( } } } + + fun selectAll() { + selected.addAll(packages.value?.map { it.packageName } ?: emptyList()) + } + + fun removeAll() { + selected.clear() + } + + fun importBlacklist(context: Context, uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + context.contentResolver.openInputStream(uri)?.use { + val importedSet: MutableSet = gson.fromJson( + it.bufferedReader().readText(), + object : TypeToken?>() {}.type + ) + + val knownSet = blacklistProvider.blacklist + knownSet.addAll(importedSet) + + selected = knownSet + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to import blacklist", exception) + } + } + } + + fun exportBlacklist(context: Context, uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + context.contentResolver.openOutputStream(uri)?.use { + it.write(gson.toJson(blacklistProvider.blacklist).encodeToByteArray()) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to export blacklist", exception) + } + } + } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt index 87ae60bea..09a42246e 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt @@ -21,6 +21,7 @@ package com.aurora.store.viewmodel.all import android.content.Context import android.net.Uri +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.store.data.room.favourite.Favourite @@ -39,26 +40,38 @@ class FavouriteViewModel @Inject constructor( private val favouriteDao: FavouriteDao, private val gson: Gson ) : ViewModel() { + private val TAG = FavouriteViewModel::class.java.simpleName val favouritesList = favouriteDao.favourites() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) fun importFavourites(context: Context, uri: Uri) { viewModelScope.launch(Dispatchers.IO) { - context.contentResolver.openInputStream(uri)?.use { - val importExport = - gson.fromJson(it.bufferedReader().readText(), ImportExport::class.java) - favouriteDao.insertAll( - importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) } - ) + try { + context.contentResolver.openInputStream(uri)?.use { + val importExport = gson.fromJson( + it.bufferedReader().readText(), + ImportExport::class.java + ) + + favouriteDao.insertAll( + importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) } + ) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to import favourites", exception) } } } fun exportFavourites(context: Context, uri: Uri) { viewModelScope.launch(Dispatchers.IO) { - context.contentResolver.openOutputStream(uri)?.use { - it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray()) + try { + context.contentResolver.openOutputStream(uri)?.use { + it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray()) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to export favourites", exception) } } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt index d16ddfa01..85373fe7a 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt @@ -20,11 +20,17 @@ package com.aurora.store.viewmodel.all import android.content.Context -import android.content.pm.PackageInfo +import android.net.Uri import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper +import com.aurora.store.data.providers.BlacklistProvider +import com.aurora.store.data.room.favourite.Favourite +import com.aurora.store.data.room.favourite.ImportExport import com.aurora.store.util.PackageUtil +import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -35,13 +41,16 @@ import javax.inject.Inject @HiltViewModel class InstalledViewModel @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val blacklistProvider: BlacklistProvider, + private val gson: Gson, + private val webAppDetailsHelper: WebAppDetailsHelper ) : ViewModel() { private val TAG = InstalledViewModel::class.java.simpleName - private val _packages = MutableStateFlow?>(null) - val packages = _packages.asStateFlow() + private val _apps = MutableStateFlow?>(null) + val apps = _apps.asStateFlow() init { fetchApps() @@ -50,10 +59,35 @@ class InstalledViewModel @Inject constructor( fun fetchApps() { viewModelScope.launch(Dispatchers.IO) { try { - _packages.value = PackageUtil.getAllValidPackages(context) + val packages = PackageUtil.getAllValidPackages(context) + .filterNot { blacklistProvider.isBlacklisted(it.packageName) } + + // Divide the list of packages into chunks of 100 & fetch app details + // 50 is a safe number to avoid hitting the rate limit or package size limit + val chunkedPackages = packages.chunked(50) + val allApps = chunkedPackages.flatMap { chunk -> + webAppDetailsHelper.getAppDetails(chunk.map { it.packageName }) + } + + _apps.emit(allApps) } catch (exception: Exception) { Log.e(TAG, "Failed to fetch apps", exception) } } } + + fun exportApps(context: Context, uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + val favourites: List = apps.value!!.map { app -> + Favourite.fromApp(app, Favourite.Mode.IMPORT) + } + context.contentResolver.openOutputStream(uri)?.use { + it.write(gson.toJson(ImportExport(favourites)).encodeToByteArray()) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to installed apps", exception) + } + } + } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt index 469b9e33d..5e60fee9e 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt @@ -1,9 +1,11 @@ package com.aurora.store.viewmodel.details +import android.content.Context import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.Constants +import com.aurora.extensions.toast import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.details.TestingProgramStatus @@ -11,19 +13,26 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper import com.aurora.gplayapi.network.IHttpClient +import com.aurora.store.AuroraApp import com.aurora.store.BuildConfig +import com.aurora.store.R import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.model.ExodusReport import com.aurora.store.data.model.Report import com.aurora.store.data.room.favourite.Favourite import com.aurora.store.data.room.favourite.FavouriteDao +import com.aurora.store.util.PackageUtil import com.google.gson.GsonBuilder import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.json.JSONObject import java.lang.reflect.Modifier @@ -32,6 +41,7 @@ import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport @HiltViewModel class AppDetailsViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val appDetailsHelper: AppDetailsHelper, private val reviewsHelper: ReviewsHelper, private val webDataSafetyHelper: WebDataSafetyHelper, @@ -69,7 +79,10 @@ class AppDetailsViewModel @Inject constructor( private val _favourite = MutableStateFlow(false) val favourite = _favourite.asStateFlow() - val downloadsList get() = downloadHelper.downloadsList + val download = combine(app, downloadHelper.downloadsList) { a, list -> + if (a.packageName.isBlank()) return@combine null + list.find { d -> d.packageName == a.packageName } + }.stateIn(viewModelScope, SharingStarted.Eagerly, null) fun fetchAppDetails(packageName: String) { viewModelScope.launch(Dispatchers.IO) { @@ -77,7 +90,9 @@ class AppDetailsViewModel @Inject constructor( checkFavourite(packageName) val app: App = appStash.getOrPut(packageName) { - appDetailsHelper.getAppByPackageName(packageName) + appDetailsHelper.getAppByPackageName(packageName).apply { + isInstalled = PackageUtil.isInstalled(context, packageName) + } } _app.emit(app) @@ -148,20 +163,42 @@ class AppDetailsViewModel @Inject constructor( } } + fun fetchUserAppReview(app: App) { + viewModelScope.launch(Dispatchers.IO) { + try { + val stashedUserReview = userReviewStash[app.packageName] + if (stashedUserReview != null) { + _userReview.emit(stashedUserReview) + return@launch + } + + val isTesting = app.testingProgram?.isSubscribed ?: false + val userReview = reviewsHelper.getUserReview(app.packageName, isTesting) + + if (userReview != null) { + userReviewStash[app.packageName] = userReview + _userReview.emit(userReview) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to fetch user review", exception) + } + } + } + fun postAppReview(packageName: String, review: Review, isBeta: Boolean) { viewModelScope.launch(Dispatchers.IO) { try { - val userReview = userReviewStash.getOrPut(packageName) { - reviewsHelper.addOrEditReview( - packageName, - review.title, - review.comment, - review.rating, - isBeta - ) - } + val userReview = reviewsHelper.addOrEditReview( + packageName, + review.title, + review.comment, + review.rating, + isBeta + ) if (userReview != null) { + context.toast(R.string.toast_rated_success) + userReviewStash[packageName] = userReview _userReview.emit(userReview) } } catch (exception: Exception) { @@ -206,8 +243,8 @@ class AppDetailsViewModel @Inject constructor( private fun getLatestExodusReport(packageName: String): Report? { val headers: MutableMap = mutableMapOf() - headers["Content-Type"] = "application/json" - headers["Accept"] = "application/json" + headers["Content-Type"] = Constants.JSON_MIME_TYPE + headers["Accept"] = Constants.JSON_MIME_TYPE headers["Authorization"] = "Token ${BuildConfig.EXODUS_API_KEY}" val url = Constants.EXODUS_SEARCH_URL + packageName diff --git a/app/src/main/res/drawable/bg_bottomsheet.xml b/app/src/main/res/drawable/bg_bottomsheet.xml deleted file mode 100644 index aa2832c4a..000000000 --- a/app/src/main/res/drawable/bg_bottomsheet.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_cancel.xml b/app/src/main/res/drawable/bg_cancel.xml deleted file mode 100644 index c5d625d38..000000000 --- a/app/src/main/res/drawable/bg_cancel.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/bg_rounded_outlined.xml b/app/src/main/res/drawable/bg_rounded_outlined.xml deleted file mode 100644 index 41c3e6a78..000000000 --- a/app/src/main/res/drawable/bg_rounded_outlined.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/bg_rounded_transparent.xml b/app/src/main/res/drawable/bg_rounded_transparent.xml deleted file mode 100644 index 49c544924..000000000 --- a/app/src/main/res/drawable/bg_rounded_transparent.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/filter_chip_background.xml b/app/src/main/res/drawable/filter_chip_background.xml deleted file mode 100644 index 78ec47234..000000000 --- a/app/src/main/res/drawable/filter_chip_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_apk_install.xml b/app/src/main/res/drawable/ic_apk_install.xml new file mode 100644 index 000000000..437ecda63 --- /dev/null +++ b/app/src/main/res/drawable/ic_apk_install.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout-land/fragment_about.xml b/app/src/main/res/layout-land/fragment_about.xml index 802a769d8..3f97cc0f5 100644 --- a/app/src/main/res/layout-land/fragment_about.xml +++ b/app/src/main/res/layout-land/fragment_about.xml @@ -27,9 +27,14 @@ android:orientation="vertical" tools:context=".view.ui.about.AboutFragment"> - + - + - \ No newline at end of file + diff --git a/app/src/main/res/layout-land/fragment_splash.xml b/app/src/main/res/layout-land/fragment_splash.xml index 36c580854..1dd28284d 100644 --- a/app/src/main/res/layout-land/fragment_splash.xml +++ b/app/src/main/res/layout-land/fragment_splash.xml @@ -28,9 +28,13 @@ android:weightSum="2" tools:context=".view.ui.splash.SplashFragment"> - + - + - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 77ae231ef..d3b6468e7 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -31,9 +31,14 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - - diff --git a/app/src/main/res/layout/fragment_details_more.xml b/app/src/main/res/layout/fragment_details_more.xml index 448205ced..2519b71bc 100644 --- a/app/src/main/res/layout/fragment_details_more.xml +++ b/app/src/main/res/layout/fragment_details_more.xml @@ -25,9 +25,13 @@ android:orientation="vertical" tools:context=".view.ui.details.DetailsMoreFragment"> - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_details_review.xml b/app/src/main/res/layout/fragment_details_review.xml index 56077d621..75f4ba8ce 100644 --- a/app/src/main/res/layout/fragment_details_review.xml +++ b/app/src/main/res/layout/fragment_details_review.xml @@ -25,9 +25,13 @@ android:orientation="vertical" tools:context=".view.ui.details.DetailsReviewFragment"> - + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_dev_profile.xml b/app/src/main/res/layout/fragment_dev_profile.xml index 790772d62..5a79228c6 100644 --- a/app/src/main/res/layout/fragment_dev_profile.xml +++ b/app/src/main/res/layout/fragment_dev_profile.xml @@ -27,10 +27,13 @@ android:showDividers="middle" tools:context=".view.ui.details.DevProfileFragment"> - - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_download.xml b/app/src/main/res/layout/fragment_download.xml index ff09bfc1d..abbb3894a 100644 --- a/app/src/main/res/layout/fragment_download.xml +++ b/app/src/main/res/layout/fragment_download.xml @@ -25,9 +25,15 @@ android:orientation="vertical" tools:context=".view.ui.downloads.DownloadFragment"> - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_generic_with_search.xml b/app/src/main/res/layout/fragment_generic_with_search.xml index fdba04491..9289fbf15 100644 --- a/app/src/main/res/layout/fragment_generic_with_search.xml +++ b/app/src/main/res/layout/fragment_generic_with_search.xml @@ -24,18 +24,35 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_generic_with_toolbar.xml b/app/src/main/res/layout/fragment_generic_with_toolbar.xml index ed2126f86..af5f55429 100644 --- a/app/src/main/res/layout/fragment_generic_with_toolbar.xml +++ b/app/src/main/res/layout/fragment_generic_with_toolbar.xml @@ -23,17 +23,21 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_search_result.xml b/app/src/main/res/layout/fragment_search_result.xml index 80cf9a3f1..6f96a2055 100644 --- a/app/src/main/res/layout/fragment_search_result.xml +++ b/app/src/main/res/layout/fragment_search_result.xml @@ -30,15 +30,33 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + + + + android:layout_below="@id/toolbar" /> - + + + + + android:layout_below="@id/toolbar" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml index d93b2240b..1b81ecfc6 100644 --- a/app/src/main/res/layout/fragment_splash.xml +++ b/app/src/main/res/layout/fragment_splash.xml @@ -28,9 +28,13 @@ android:weightSum="2" tools:context=".view.ui.splash.SplashFragment"> - + - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_top_chart.xml b/app/src/main/res/layout/fragment_top_chart.xml index b23743fe9..418e97b82 100644 --- a/app/src/main/res/layout/fragment_top_chart.xml +++ b/app/src/main/res/layout/fragment_top_chart.xml @@ -33,8 +33,7 @@ android:id="@+id/top_tab_group" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingStart="@dimen/padding_normal" - android:paddingEnd="@dimen/padding_normal" + android:padding="@dimen/padding_medium" app:chipSpacingHorizontal="@dimen/margin_small" app:selectionRequired="true" app:singleLine="true" @@ -42,14 +41,14 @@ @@ -57,14 +56,14 @@ diff --git a/app/src/main/res/layout/layout_details_app.xml b/app/src/main/res/layout/layout_details_app.xml index 9c908cf0c..d04bb6b16 100644 --- a/app/src/main/res/layout/layout_details_app.xml +++ b/app/src/main/res/layout/layout_details_app.xml @@ -18,6 +18,7 @@ --> - + + + + + + + @@ -84,4 +102,31 @@ android:layout_alignEnd="@id/txt_line1" android:textAppearance="@style/TextAppearance.Aurora.Line2" tools:text="Free" /> - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/layout/layout_details_beta.xml b/app/src/main/res/layout/layout_details_beta.xml index 9e961d51b..f423cef5f 100644 --- a/app/src/main/res/layout/layout_details_beta.xml +++ b/app/src/main/res/layout/layout_details_beta.xml @@ -22,9 +22,11 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:divider="@drawable/divider" android:orientation="vertical" android:paddingStart="@dimen/padding_small" android:paddingEnd="@dimen/padding_small" + android:showDividers="middle" android:visibility="gone" tools:visibility="visible"> @@ -40,7 +42,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:layout_marginTop="@dimen/margin_small" android:orientation="vertical"> diff --git a/app/src/main/res/layout/layout_details_description.xml b/app/src/main/res/layout/layout_details_description.xml index 434963328..b0b46d01f 100644 --- a/app/src/main/res/layout/layout_details_description.xml +++ b/app/src/main/res/layout/layout_details_description.xml @@ -34,70 +34,52 @@ android:orientation="vertical"> - + android:paddingStart="@dimen/padding_medium" + android:paddingEnd="@dimen/padding_medium" + app:selectionRequired="false" + app:singleLine="true"> - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_details_privacy.xml b/app/src/main/res/layout/layout_details_privacy.xml index 0d757003b..00dc5b3b7 100644 --- a/app/src/main/res/layout/layout_details_privacy.xml +++ b/app/src/main/res/layout/layout_details_privacy.xml @@ -19,7 +19,7 @@ ~ --> - - - + diff --git a/app/src/main/res/layout/layout_details_review.xml b/app/src/main/res/layout/layout_details_review.xml index 26d752bb1..5ee028729 100644 --- a/app/src/main/res/layout/layout_details_review.xml +++ b/app/src/main/res/layout/layout_details_review.xml @@ -38,8 +38,9 @@ android:id="@+id/layout_user_review" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:orientation="vertical"> + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> - + android:layout_height="wrap_content"> - + + + + android:layout_height="wrap_content"> + + + + app:cornerRadius="@dimen/radius_small" /> diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml deleted file mode 100644 index 15898ab7e..000000000 --- a/app/src/main/res/layout/settings_activity.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/sheet_filter.xml b/app/src/main/res/layout/sheet_filter.xml index da4eee12d..c4f5246e1 100644 --- a/app/src/main/res/layout/sheet_filter.xml +++ b/app/src/main/res/layout/sheet_filter.xml @@ -87,21 +87,21 @@ diff --git a/app/src/main/res/layout/view_action_button.xml b/app/src/main/res/layout/view_action_button.xml deleted file mode 100644 index 8c0177610..000000000 --- a/app/src/main/res/layout/view_action_button.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/view_toolbar_action.xml b/app/src/main/res/layout/view_toolbar_action.xml deleted file mode 100644 index a06aceeb1..000000000 --- a/app/src/main/res/layout/view_toolbar_action.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/view_toolbar_native.xml b/app/src/main/res/layout/view_toolbar_native.xml deleted file mode 100644 index 266e9115b..000000000 --- a/app/src/main/res/layout/view_toolbar_native.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/view_toolbar_search.xml b/app/src/main/res/layout/view_toolbar_search.xml deleted file mode 100644 index 86d75ec3e..000000000 --- a/app/src/main/res/layout/view_toolbar_search.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_sheet.xml b/app/src/main/res/menu/menu_blacklist.xml similarity index 62% rename from app/src/main/res/drawable/bg_sheet.xml rename to app/src/main/res/menu/menu_blacklist.xml index 29774bd74..072968921 100644 --- a/app/src/main/res/drawable/bg_sheet.xml +++ b/app/src/main/res/menu/menu_blacklist.xml @@ -17,11 +17,17 @@ ~ --> - - - - - - - - \ No newline at end of file + + + + + + diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml new file mode 100644 index 000000000..fd45127de --- /dev/null +++ b/app/src/main/res/menu/menu_search.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 249a94b0f..c69f436b5 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -71,12 +71,12 @@ android:id="@+id/appsGamesFragment" android:name="com.aurora.store.view.ui.all.AppsGamesFragment" android:label="@string/title_apps_games" - tools:layout="@layout/fragment_generic_with_pager" /> + tools:layout="@layout/fragment_generic_with_search" /> + tools:layout="@layout/fragment_spoof" /> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 17f8959e7..abde5e933 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -28,12 +28,6 @@ - - - - - - @@ -48,4 +42,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c931f503..72d6baf6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,10 +85,12 @@ "Pause" "Pending" "Post" + "Remove all" Request new analysis "Restart" "Resume" "Search" + "Select all" "Share" "Uninstall" "Successfully uninstalled" @@ -156,6 +158,7 @@ known trackers(s) found in "View report" "All" + "Latest" "Critical" "Five" "Four" @@ -437,6 +440,12 @@ Favourites imported! Favourites exported! + + Failed to import blacklist! + Failed to export blacklist! + Blacklist imported! + Blacklist exported! + File Exporter Hold on, exporting your file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 01191ab05..50087a0f7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -20,7 +20,7 @@ - + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4376bf31e..c7f568f88 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ composeBom = "2024.11.00" coreVersion = "1.15.0" epoxyVersion = "5.1.4" espressoVersion = "3.6.1" -gplayapiVersion = "3.4.3" +gplayapiVersion = "3.4.4" gsonVersion = "2.11.0" hiddenapibypassVersion = "4.3" hiltVersion = "2.53"