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"