Merge branch 'dev'

This commit is contained in:
Aayush Gupta
2024-12-19 11:56:01 +07:00
80 changed files with 1406 additions and 1398 deletions

View File

@@ -46,6 +46,12 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission
android:name="android.permission.USE_CREDENTIALS"
android:maxSdkVersion="22" />
<uses-feature <uses-feature
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="false" /> android:required="false" />
@@ -81,7 +87,8 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -35,7 +35,8 @@ object Constants {
const val SHARE_URL = "https://play.google.com/store/apps/details?id=" 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_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_EXPORT = "NOTIFICATION_CHANNEL_EXPORT"
const val NOTIFICATION_CHANNEL_INSTALL = "NOTIFICATION_CHANNEL_INSTALL" const val NOTIFICATION_CHANNEL_INSTALL = "NOTIFICATION_CHANNEL_INSTALL"
@@ -56,4 +57,6 @@ object Constants {
const val PAGE_TYPE = "PAGE_TYPE" const val PAGE_TYPE = "PAGE_TYPE"
const val TOP_CHART_TYPE = "TOP_CHART_TYPE" const val TOP_CHART_TYPE = "TOP_CHART_TYPE"
const val TOP_CHART_CATEGORY = "TOP_CHART_CATEGORY" const val TOP_CHART_CATEGORY = "TOP_CHART_CATEGORY"
const val JSON_MIME_TYPE = "application/json"
} }

View File

@@ -1,14 +1,23 @@
package com.aurora.extensions package com.aurora.extensions
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Process import android.os.Process
import androidx.core.content.pm.PackageInfoCompat
fun PackageInfo.isValidApp(packageManager: PackageManager): Boolean { fun PackageInfo.isValidApp(packageManager: PackageManager): Boolean {
if (this.applicationInfo == null || this.packageName.isEmpty()) return false if (this.applicationInfo == null || this.packageName.isEmpty()) return false
// Most core AOSP system apps use their package name as label // Filter out core AOSP system apps
if (this.applicationInfo!!.loadLabel(packageManager).startsWith(this.packageName)) return false 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 { return when {
isQAndAbove -> { isQAndAbove -> {

View File

@@ -82,7 +82,7 @@ class MainActivity : AppCompatActivity() {
// Adjust root view's paddings for edgeToEdge display // Adjust root view's paddings for edgeToEdge display
ViewCompat.setOnApplyWindowInsetsListener(B.root) { root, windowInsets -> ViewCompat.setOnApplyWindowInsetsListener(B.root) { root, windowInsets ->
val insets = windowInsets.getInsets(systemBars() or displayCutout() or ime()) 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 windowInsets
} }

View File

@@ -23,12 +23,3 @@ enum class AccountType {
ANONYMOUS, ANONYMOUS,
GOOGLE GOOGLE
} }
enum class State {
IDLE,
QUEUED,
PROGRESS,
COMPLETE,
CANCELED,
INSTALLING,
}

View File

@@ -2,34 +2,61 @@ package com.aurora.store.data.model
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Parcelable import android.os.Parcelable
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.data.room.update.Update import com.aurora.store.data.room.update.Update
import com.aurora.store.util.PackageUtil
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class MinimalApp( data class MinimalApp(
val packageName: String, val packageName: String,
val versionName: String,
val versionCode: Int, val versionCode: Int,
val displayName: String val displayName: String,
@IgnoredOnParcel
val icon: Bitmap? = null
) : Parcelable { ) : Parcelable {
companion object { companion object {
fun fromApp(app: App): MinimalApp { 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 { 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 { fun fromPackageInfo(context: Context, packageInfo: PackageInfo): MinimalApp {
return MinimalApp( return MinimalApp(
packageInfo.packageName, packageInfo.packageName,
packageInfo.versionName ?: "",
PackageInfoCompat.getLongVersionCode(packageInfo).toInt(), PackageInfoCompat.getLongVersionCode(packageInfo).toInt(),
packageInfo.applicationInfo!!.loadLabel(context.packageManager).toString() packageInfo.applicationInfo!!.loadLabel(context.packageManager).toString(),
PackageUtil.getIconForPackage(context, packageInfo.packageName)
) )
} }
} }

View File

@@ -22,6 +22,7 @@ package com.aurora.store.data.network
import android.content.Context import android.content.Context
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.Algorithm import com.aurora.store.data.model.Algorithm
import com.aurora.store.data.model.ProxyInfo import com.aurora.store.data.model.ProxyInfo
@@ -60,16 +61,20 @@ object OkHttpClientModule {
@Provides @Provides
@Singleton @Singleton
fun providesOkHttpClientInstance(certPinner: CertificatePinner, proxy: Proxy?): OkHttpClient { fun providesOkHttpClientInstance(certPinner: CertificatePinner, proxy: Proxy?): OkHttpClient {
return OkHttpClient().newBuilder() val okHttpClientBuilder = OkHttpClient().newBuilder()
.proxy(proxy) .proxy(proxy)
.certificatePinner(certPinner)
.connectTimeout(25, TimeUnit.SECONDS) .connectTimeout(25, TimeUnit.SECONDS)
.readTimeout(25, TimeUnit.SECONDS) .readTimeout(25, TimeUnit.SECONDS)
.writeTimeout(25, TimeUnit.SECONDS) .writeTimeout(25, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) .retryOnConnectionFailure(true)
.followRedirects(true) .followRedirects(true)
.followSslRedirects(true) .followSslRedirects(true)
.build()
if (!BuildConfig.DEBUG) {
okHttpClientBuilder.certificatePinner(certPinner)
}
return okHttpClientBuilder.build()
} }
@Provides @Provides

View File

@@ -3,6 +3,8 @@ package com.aurora.store.data.room.favourite
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Artwork
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -16,6 +18,26 @@ data class Favourite(
val mode: Mode val mode: Mode
) : Parcelable { ) : 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 { enum class Mode {
MANUAL, MANUAL,
IMPORT IMPORT

View File

@@ -1,7 +1,13 @@
package com.aurora.store.data.work package com.aurora.store.data.work
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log import android.util.Log
import androidx.core.os.bundleOf
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters 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.model.AccountType
import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AccountProvider
import com.aurora.store.data.providers.AuthProvider 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.Assisted
import dagger.assisted.AssistedInject 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 * Worker to refresh [AuthData] in background
@@ -26,6 +40,8 @@ open class AuthWorker @AssistedInject constructor(
private val TAG = AuthWorker::class.java.simpleName private val TAG = AuthWorker::class.java.simpleName
private val authToken: MutableSharedFlow<String?> = MutableSharedFlow(extraBufferCapacity = 1)
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
if (!AccountProvider.isLoggedIn(appContext)) { if (!AccountProvider.isLoggedIn(appContext)) {
Log.i(TAG, "User has logged out!") Log.i(TAG, "User has logged out!")
@@ -42,14 +58,48 @@ open class AuthWorker @AssistedInject constructor(
val accountType = AccountProvider.getAccountType(appContext) val accountType = AccountProvider.getAccountType(appContext)
val authData = when (accountType) { val authData = when (accountType) {
AccountType.GOOGLE -> { AccountType.GOOGLE -> {
authProvider.buildGoogleAuthData( val email = AccountProvider.getLoginEmail(appContext)!!
AccountProvider.getLoginEmail(appContext)!!, val token = AccountProvider.getLoginToken(appContext)!!.first
AccountProvider.getLoginToken(appContext)!!.first, val tokenType = AccountProvider.getLoginToken(appContext)!!.second
AccountProvider.getLoginToken(appContext)!!.second
).getOrThrow() 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) require(verifyAndSaveAuth(authData, accountType) != null)

View File

@@ -7,6 +7,7 @@ import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.SearchHelper
import com.aurora.gplayapi.helpers.StreamHelper 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.WebCategoryStreamHelper
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.gplayapi.helpers.web.WebSearchHelper import com.aurora.gplayapi.helpers.web.WebSearchHelper
@@ -152,4 +153,15 @@ object HelperModule {
.using(httpClient) .using(httpClient)
.with(spoofProvider.locale) .with(spoofProvider.locale)
} }
@Singleton
@Provides
fun providesWebAppDetailsHelperInstance(
spoofProvider: SpoofProvider,
httpClient: IHttpClient
): WebAppDetailsHelper {
return WebAppDetailsHelper()
.using(httpClient)
.with(spoofProvider.locale)
}
} }

View File

@@ -32,11 +32,27 @@ import com.aurora.store.data.model.Algorithm
import com.aurora.store.util.PackageUtil.getPackageInfo import com.aurora.store.util.PackageUtil.getPackageInfo
import java.security.MessageDigest import java.security.MessageDigest
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal
object CertUtil { object CertUtil {
private val TAG = "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 { fun isFDroidApp(context: Context, packageName: String): Boolean {
return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName) 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 { private fun isInstalledByFDroid(context: Context, packageName: String): Boolean {
val fdroidPackages = listOf( val fdroidPackages = listOf(
"org.fdroid.basic", "org.fdroid.fdroid", "org.fdroid.fdroid.privileged" "org.fdroid.basic", "org.fdroid.fdroid", "org.fdroid.fdroid.privileged"
@@ -120,4 +161,19 @@ object CertUtil {
getPackageInfo(context, packageName, PackageManager.GET_SIGNATURES) 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<String, String> {
return principal.name.split(",").associate {
val (left, right) = it.split("=")
left.trim() to right.trim()
}
}
} }

View File

@@ -27,11 +27,13 @@ import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags import android.content.pm.PackageManager.PackageInfoFlags
import android.content.pm.SharedLibraryInfo import android.content.pm.SharedLibraryInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.aurora.extensions.isHuawei import com.aurora.extensions.isHuawei
@@ -40,12 +42,17 @@ import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isTAndAbove import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isValidApp import com.aurora.extensions.isValidApp
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R
import java.util.Locale import java.util.Locale
object PackageUtil { object PackageUtil {
private const val TAG = "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<PackageInfo> { fun getAllValidPackages(context: Context): List<PackageInfo> {
val sharedLibs = context.packageManager.systemSharedLibraryNames ?: emptyArray() val sharedLibs = context.packageManager.systemSharedLibraryNames ?: emptyArray()
return context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA) 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 { fun isInstalled(context: Context, packageName: String): Boolean {
return try { return try {
getPackageInfo(context, packageName, PackageManager.GET_META_DATA) 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<SharedLibraryInfo> { private fun getAllSharedLibraries(context: Context, flags: Int = 0): List<SharedLibraryInfo> {
return if (isTAndAbove) { return if (isTAndAbove) {
context.packageManager.getSharedLibraries(PackageInfoFlags.of(flags.toLong())) context.packageManager.getSharedLibraries(PackageInfoFlags.of(flags.toLong()))

View File

@@ -1,109 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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)
}
}

View File

@@ -20,9 +20,7 @@
package com.aurora.store.view.epoxy.views package com.aurora.store.view.epoxy.views
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.content.pm.PackageInfoCompat
import coil3.load import coil3.load
import coil3.request.placeholder import coil3.request.placeholder
import coil3.request.transformations import coil3.request.transformations
@@ -31,30 +29,29 @@ import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView import com.airbnb.epoxy.ModelView
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.MinimalApp
import com.aurora.store.databinding.ViewPackageBinding import com.aurora.store.databinding.ViewPackageBinding
import com.aurora.store.util.PackageUtil
@ModelView( @ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class baseModelClass = BaseModel::class
) )
class PackageInfoView @JvmOverloads constructor( class InstalledAppView @JvmOverloads constructor(
context: Context?, context: Context?,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : BaseView<ViewPackageBinding>(context, attrs, defStyleAttr) { ) : BaseView<ViewPackageBinding>(context, attrs, defStyleAttr) {
@ModelProp(options = [ModelProp.Option.IgnoreRequireHashCode]) @ModelProp(options = [ModelProp.Option.IgnoreRequireHashCode])
fun packageInfo(packageInfo: PackageInfo) { fun packageInfo(app: MinimalApp) {
val appInfo = packageInfo.applicationInfo!! binding.imgIcon.load(app.icon) {
binding.imgIcon.load(PackageUtil.getIconForPackage(context, appInfo.packageName)) {
placeholder(R.drawable.bg_placeholder) placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(25F)) transformations(RoundedCornersTransformation(25F))
} }
binding.txtLine1.text = appInfo.loadLabel(context.packageManager) binding.txtLine1.text = app.displayName
binding.txtLine2.text = appInfo.packageName binding.txtLine2.text = app.packageName
binding.txtLine3.text = ("${packageInfo.versionName}.${PackageInfoCompat.getLongVersionCode(packageInfo)}") binding.txtLine3.text = ("${app.versionName} (${app.versionCode})")
} }
@CallbackProp @CallbackProp

View File

@@ -42,10 +42,7 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Toolbar // Toolbar
binding.layoutToolbarAction.txtTitle.text = getString(R.string.title_about) binding.toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
binding.layoutToolbarAction.imgActionPrimary.setOnClickListener {
findNavController().navigateUp()
}
// About Details // About Details
binding.imgIcon.load(R.mipmap.ic_launcher) binding.imgIcon.load(R.mipmap.ic_launcher)

View File

@@ -47,10 +47,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Toolbar // Toolbar
binding.layoutToolbarAction.txtTitle.text = getString(R.string.title_account_manager) binding.toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
binding.layoutToolbarAction.imgActionPrimary.setOnClickListener {
findNavController().navigateUp()
}
// Chips // Chips
view.context.apply { view.context.apply {

View File

@@ -19,36 +19,47 @@
package com.aurora.store.view.ui.all package com.aurora.store.view.ui.all
import android.content.pm.PackageInfo import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController 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.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.MinimalApp import com.aurora.store.data.model.MinimalApp
import com.aurora.store.databinding.FragmentGenericWithSearchBinding import com.aurora.store.databinding.FragmentGenericWithSearchBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_ 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.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.all.InstalledViewModel import com.aurora.store.viewmodel.all.InstalledViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Calendar
@AndroidEntryPoint @AndroidEntryPoint
class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() { class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
private val viewModel: InstalledViewModel by viewModels() 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.packages.collect { viewModel.apps.collect {
updateController(it) updateController(it)
} }
} }
@@ -67,38 +78,47 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
} }
// Toolbar // Toolbar
binding.layoutToolbarNative.apply { binding.toolbar.apply {
imgActionPrimary.visibility = View.VISIBLE inflateMenu(R.menu.menu_import_export)
imgActionSecondary.visibility = View.GONE setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener {
imgActionPrimary.setOnClickListener { findNavController().navigateUp() } when (it.itemId) {
R.id.action_export -> {
inputSearch.addTextChangedListener(object : TextWatcher { startForDocumentExport.launch(
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { "aurora_store_apps_${Calendar.getInstance().time.time}.json"
if (s.isNullOrEmpty()) { )
updateController(viewModel.packages.value) true
} 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?) {} else -> false
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
} }
}) }
} }
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<PackageInfo>?) { private fun updateController(packages: List<App>?) {
binding.recycler.withModels { binding.recycler.withModels {
setFilterDuplicates(true) setFilterDuplicates(true)
if (packages == null) { if (packages == null) {
@@ -116,12 +136,21 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
) )
packages.forEach { app -> packages.forEach { app ->
add( add(
PackageInfoViewModel_() AppListViewModel_()
.id(app.packageName.hashCode()) .id(app.packageName.hashCode())
.packageInfo(app) .app(app)
.click { _ -> openDetailsFragment(app.packageName) } .click { _ ->
openDetailsFragment(
app.packageName,
app
)
}
.longClick { _ -> .longClick { _ ->
openAppMenuSheet(MinimalApp.fromPackageInfo(requireContext(), app)) openAppMenuSheet(
MinimalApp.fromApp(
app
)
)
false false
} }
) )
@@ -130,4 +159,8 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
} }
} }
private fun exportInstalledApps(uri: Uri) {
viewModel.exportApps(requireContext(), uri)
toast(R.string.toast_fav_export_success)
}
} }

View File

@@ -20,14 +20,19 @@
package com.aurora.store.view.ui.commons package com.aurora.store.view.ui.commons
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.databinding.FragmentGenericWithSearchBinding import com.aurora.store.databinding.FragmentGenericWithSearchBinding
import com.aurora.store.view.epoxy.views.BlackListViewModel_ 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 com.aurora.store.viewmodel.all.BlacklistViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Calendar
@AndroidEntryPoint @AndroidEntryPoint
class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() { class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
private val viewModel: BlacklistViewModel by viewModels() 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -51,37 +66,62 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
} }
// Toolbar // Toolbar
binding.layoutToolbarNative.apply { binding.toolbar.apply {
imgActionPrimary.visibility = View.VISIBLE inflateMenu(R.menu.menu_blacklist)
imgActionSecondary.visibility = View.GONE setNavigationOnClickListener {
imgActionPrimary.setOnClickListener {
viewModel.blacklistProvider.blacklist = viewModel.selected viewModel.blacklistProvider.blacklist = viewModel.selected
findNavController().navigateUp() findNavController().navigateUp()
} }
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_import -> {
startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE))
}
inputSearch.addTextChangedListener(object : TextWatcher { R.id.action_export -> {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { startForDocumentExport.launch(
if (s.isNullOrEmpty()) { "aurora_store_apps_${Calendar.getInstance().time.time}.json"
updateController(viewModel.packages.value) )
} else { }
val filteredPackages = viewModel.packages.value?.filter {
it.applicationInfo!!.loadLabel(requireContext().packageManager) R.id.action_select_all -> {
.contains(s, true) || it.packageName.contains(s, true) viewModel.selectAll()
} binding.recycler.requestModelBuild()
updateController(filteredPackages) true
}
R.id.action_remove_all -> {
viewModel.removeAll()
binding.recycler.requestModelBuild()
true
} }
} }
true
override fun afterTextChanged(s: Editable?) {} }
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {}
})
} }
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() { override fun onPause() {
@@ -125,4 +165,15 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
} }
} }
} }
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)
}
} }

View File

@@ -21,7 +21,6 @@ package com.aurora.store.view.ui.commons
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs 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.data.models.StreamCluster
import com.aurora.gplayapi.helpers.contracts.StreamContract import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.gplayapi.utils.CategoryUtil 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
import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
@@ -57,9 +55,8 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
val genericCarouselController = CategoryCarouselController(this) val genericCarouselController = CategoryCarouselController(this)
// Toolbar // Toolbar
binding.layoutToolbarNative.toolbar.apply { binding.toolbar.apply {
title = args.title title = args.title
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
} }

View File

@@ -21,13 +21,11 @@ package com.aurora.store.view.ui.commons
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_ import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_
@@ -51,9 +49,8 @@ class ExpandedStreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBind
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Toolbar // Toolbar
binding.layoutToolbarNative.toolbar.apply { binding.toolbar.apply {
title = args.title title = args.title
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
} }
@@ -71,7 +68,7 @@ class ExpandedStreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBind
private fun updateTitle(streamCluster: StreamCluster) { private fun updateTitle(streamCluster: StreamCluster) {
if (streamCluster.clusterTitle.isNotEmpty()) if (streamCluster.clusterTitle.isNotEmpty())
binding.layoutToolbarNative.toolbar.title = streamCluster.clusterTitle binding.toolbar.title = streamCluster.clusterTitle
} }
private fun attachRecycler() { private fun attachRecycler() {

View File

@@ -26,9 +26,11 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.aurora.Constants
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.room.favourite.Favourite import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.Favourite.Companion.toApp
import com.aurora.store.databinding.FragmentFavouriteBinding import com.aurora.store.databinding.FragmentFavouriteBinding
import com.aurora.store.view.epoxy.views.FavouriteViewModel_ import com.aurora.store.view.epoxy.views.FavouriteViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ import com.aurora.store.view.epoxy.views.app.NoAppViewModel_
@@ -42,14 +44,13 @@ import java.util.Calendar
class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() { class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() {
private val viewModel: FavouriteViewModel by viewModels() private val viewModel: FavouriteViewModel by viewModels()
private val mimeType = "application/json"
private val startForDocumentImport = private val startForDocumentImport =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { 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 = private val startForDocumentExport =
registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) {
if (it != null) exportDeviceConfig(it) else toast(R.string.toast_fav_export_failed) if (it != null) exportFavourites(it) else toast(R.string.toast_fav_export_failed)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -65,7 +66,7 @@ class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() {
binding.toolbar.apply { binding.toolbar.apply {
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { 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 -> { R.id.action_export -> {
startForDocumentExport.launch( startForDocumentExport.launch(
"aurora_store_favourites_${Calendar.getInstance().time.time}.json" "aurora_store_favourites_${Calendar.getInstance().time.time}.json"
@@ -103,7 +104,7 @@ class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() {
FavouriteViewModel_() FavouriteViewModel_()
.id(it.packageName.hashCode()) .id(it.packageName.hashCode())
.favourite(it) .favourite(it)
.onClick { _ -> openDetailsFragment(it.packageName) } .onClick { _ -> openDetailsFragment(it.packageName, it.toApp()) }
.onFavourite { _ -> viewModel.removeFavourite(it.packageName) } .onFavourite { _ -> viewModel.removeFavourite(it.packageName) }
) )
} }
@@ -111,13 +112,13 @@ class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() {
} }
} }
private fun importDeviceConfig(uri: Uri) { private fun importFavourites(uri: Uri) {
viewModel.importFavourites(requireContext(), uri) viewModel.importFavourites(requireContext(), uri)
binding.recycler.requestModelBuild() binding.recycler.requestModelBuild()
toast(R.string.toast_fav_import_success) toast(R.string.toast_fav_import_success)
} }
private fun exportDeviceConfig(uri: Uri) { private fun exportFavourites(uri: Uri) {
viewModel.exportFavourites(requireContext(), uri) viewModel.exportFavourites(requireContext(), uri)
toast(R.string.toast_fav_export_success) toast(R.string.toast_fav_export_success)
} }

View File

@@ -21,12 +21,10 @@ package com.aurora.store.view.ui.commons
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppProgressViewModel_ import com.aurora.store.view.epoxy.views.AppProgressViewModel_
@@ -48,9 +46,8 @@ class StreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
streamCluster = args.cluster streamCluster = args.cluster
// Toolbar // Toolbar
binding.layoutToolbarNative.toolbar.apply { binding.toolbar.apply {
title = streamCluster.clusterTitle title = streamCluster.clusterTitle
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
} }

View File

@@ -20,33 +20,35 @@
package com.aurora.store.view.ui.details package com.aurora.store.view.ui.details
import android.animation.ObjectAnimator
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.view.ViewCompat import androidx.core.view.isVisible
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import coil3.asDrawable
import coil3.load import coil3.load
import coil3.request.placeholder import coil3.request.error
import coil3.request.transformations import coil3.request.transformations
import coil3.transform.CircleCropTransformation
import coil3.transform.RoundedCornersTransformation import coil3.transform.RoundedCornersTransformation
import com.aurora.Constants import com.aurora.Constants
import com.aurora.Constants.EXODUS_SUBMIT_PAGE import com.aurora.Constants.EXODUS_SUBMIT_PAGE
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.hide import com.aurora.extensions.hide
import com.aurora.extensions.px
import com.aurora.extensions.requiresObbDir import com.aurora.extensions.requiresObbDir
import com.aurora.extensions.runOnUiThread import com.aurora.extensions.runOnUiThread
import com.aurora.extensions.share 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.installer.AppInstaller
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.PermissionType 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
import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.data.providers.AuthProvider 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.CommonUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences 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.util.ShortcutManagerUtil
import com.aurora.store.view.custom.RatingView import com.aurora.store.view.custom.RatingView
import com.aurora.store.view.epoxy.controller.DetailsCarouselController 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.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.AppDetailsViewModel import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsClusterViewModel 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 dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@@ -106,46 +108,35 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
@Inject @Inject
lateinit var authProvider: AuthProvider lateinit var authProvider: AuthProvider
private lateinit var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>
private lateinit var app: App private lateinit var app: App
private lateinit var iconDrawable: Drawable
private var streamBundle: StreamBundle? = StreamBundle() private var streamBundle: StreamBundle? = StreamBundle()
private val isExternal get() = activity?.intent?.action != Intent.ACTION_MAIN private val isExternal get() = activity?.intent?.action != Intent.ACTION_MAIN
private var downloadStatus = DownloadStatus.UNAVAILABLE private val isExtendedUpdateEnabled: Boolean
private var isUpdatable: Boolean = false get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED)
private var uninstallActionEnabled = false private val showSimilarApps: Boolean
get() = Preferences.getBoolean(requireContext(), PREFERENCE_SIMILAR)
private fun onEvent(event: Event) { private fun onEvent(event: Event) {
when (event) { when (event) {
is InstallerEvent.Installed -> { is InstallerEvent.Installed -> {
if (app.packageName == event.packageName) { if (app.packageName == event.packageName) {
attachActions() checkAndSetupInstall()
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
}
} }
} }
is InstallerEvent.Uninstalled -> { is InstallerEvent.Uninstalled -> {
if (app.packageName == event.packageName) { if (app.packageName == event.packageName) {
attachActions() checkAndSetupInstall()
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
}
} }
} }
is BusEvent.ManualDownload -> { is BusEvent.ManualDownload -> {
if (app.packageName == event.packageName) { if (app.packageName == event.packageName) {
app.versionCode = event.versionCode purchase(app.copy(versionCode = event.versionCode))
purchase()
} }
} }
@@ -164,8 +155,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
is InstallerEvent.Installing -> { is InstallerEvent.Installing -> {
if (event.packageName == app.packageName) { if (event.packageName == app.packageName) {
attachActions() checkAndSetupInstall()
updateActionState(State.INSTALLING)
} }
} }
@@ -178,35 +168,17 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Adjust margins for edgeToEdge display app = args.app ?: App(args.packageName)
ViewCompat.setOnApplyWindowInsetsListener(binding.layoutDetailsDev.root) { v, w -> app.apply {
val insets = w.getInsets(WindowInsetsCompat.Type.navigationBars()) // Check whether app is installed or not
v.updateLayoutParams<ViewGroup.MarginLayoutParams> { isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
bottomMargin += insets.bottom
}
WindowInsetsCompat.CONSUMED
} }
ViewCompat.setOnApplyWindowInsetsListener(binding.layoutDetailsInstall.viewFlipper) { v, w -> // Show the basic app details, while the rest of the data is being fetched
val insets = w.getInsets(WindowInsetsCompat.Type.navigationBars()) updateAppHeader(app, false)
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin += insets.bottom
}
WindowInsetsCompat.CONSUMED
}
if (args.app != null) {
app = args.app!!
inflatePartialApp()
} else {
app = App(args.packageName)
}
// Toolbar // Toolbar
attachToolbar() updateToolbar(app)
// Check whether app is installed or not
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
// App Details // App Details
viewModel.fetchAppDetails(app.packageName) viewModel.fetchAppDetails(app.packageName)
@@ -215,38 +187,55 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
viewModel.app.collect { viewModel.app.collect {
if (it.packageName.isNotBlank()) { if (it.packageName.isNotBlank()) {
app = it 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) viewModel.fetchAppReviews(app.packageName)
// Fetch Data Safety Report
viewModel.fetchAppDataSafetyReport(app.packageName)
// Fetch Exodus Privacy Report
viewModel.fetchAppReport(app.packageName)
} else { } else {
toast("Failed to fetch app details") toast(getString(R.string.status_unavailable))
// TODO: Redirect to App Unavailable Fragment
} }
} }
} }
viewLifecycleOwner.lifecycleScope.launch { viewModel.download.filterNotNull().onEach {
viewModel.downloadsList when (it.downloadStatus) {
.filter { list -> list.any { it.packageName == app.packageName } } DownloadStatus.QUEUED,
.collectLatest { downloadsList -> DownloadStatus.DOWNLOADING -> {
val download = downloadsList.find { it.packageName == app.packageName } updateProgress(it.progress)
download?.let { binding.layoutDetailsApp.btnPrimaryAction.apply {
downloadStatus = it.downloadStatus isEnabled = false
text = getString(R.string.action_open)
if (it.isFinished) flip(0) else flip(1) setOnClickListener { openApp() }
when (it.downloadStatus) { }
DownloadStatus.QUEUED -> { binding.layoutDetailsApp.btnSecondaryAction.apply {
updateProgress(it.progress) text = getString(R.string.action_cancel)
} setOnClickListener { viewModel.cancelDownload(app) }
DownloadStatus.DOWNLOADING -> {
updateProgress(it.progress, it.speed, it.timeRemaining)
}
else -> {}
}
} }
} }
}
else -> checkAndSetupInstall()
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
// Reviews // Reviews
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
@@ -261,12 +250,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.userReview.collect { viewModel.userReview.collect {
if (it.commentId.isNotEmpty()) { if (it.commentId.isNotEmpty()) {
binding.layoutDetailsReview.userStars.rating = it.rating.toFloat() runOnUiThread { updateUserReview(it) }
Toast.makeText(
requireContext(),
getString(R.string.toast_rated_success),
Toast.LENGTH_SHORT
).show()
} else { } else {
Toast.makeText( Toast.makeText(
requireContext(), requireContext(),
@@ -350,12 +334,10 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.favourite.collect { viewModel.favourite.collect {
if (it) { if (it) {
binding.layoutDetailsToolbar.toolbar.menu binding.toolbar.menu?.findItem(R.id.action_favourite)
?.findItem(R.id.action_favourite)
?.setIcon(R.drawable.ic_favorite_checked) ?.setIcon(R.drawable.ic_favorite_checked)
} else { } else {
binding.layoutDetailsToolbar.toolbar.menu binding.toolbar.menu?.findItem(R.id.action_favourite)
?.findItem(R.id.action_favourite)
?.setIcon(R.drawable.ic_favorite_unchecked) ?.setIcon(R.drawable.ic_favorite_unchecked)
} }
} }
@@ -368,12 +350,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
binding.layoutDetailsInstall.progressDownload.clipToOutline = true
binding.layoutDetailsInstall.imgCancel.setOnClickListener {
viewModel.cancelDownload(app)
if (downloadStatus != DownloadStatus.DOWNLOADING) flip(0)
}
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.busEvent.collect { onEvent(it) } AuroraApp.events.busEvent.collect { onEvent(it) }
} }
@@ -387,15 +363,11 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
super.onResume() super.onResume()
} }
private fun attachActions() { private fun updateToolbar(app: App) {
flip(0) binding.toolbar.apply {
checkAndSetupInstall()
}
private fun attachToolbar() {
binding.layoutDetailsToolbar.toolbar.apply {
elevation = 0f elevation = 0f
navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_arrow_back) navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_arrow_back)
setNavigationOnClickListener { setNavigationOnClickListener {
if (isExternal) { if (isExternal) {
activity?.finish() activity?.finish()
@@ -404,8 +376,23 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
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 { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.action_home_screen -> { R.id.action_home_screen -> {
@@ -451,88 +438,56 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
requireContext().browse("${Constants.SHARE_URL}${app.packageName}") requireContext().browse("${Constants.SHARE_URL}${app.packageName}")
} }
} }
true 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 { 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) { imgIcon.load(app.iconArtwork.url) {
placeholder(R.drawable.bg_placeholder) error(fallbackDrawable)
transformations(RoundedCornersTransformation(32F)) transformations(RoundedCornersTransformation(32F))
listener { _, result ->
result.image.asDrawable(resources).let { iconDrawable = it }
}
} }
packageName.text = app.packageName
txtLine1.text = app.displayName txtLine1.text = app.displayName
txtLine2.text = app.developerName txtLine2.text = app.developerName
txtLine3.text = ("${app.versionName} (${app.versionCode})")
txtLine2.setOnClickListener { txtLine2.setOnClickListener {
findNavController().navigate( findNavController().navigate(
AppDetailsFragmentDirections AppDetailsFragmentDirections
.actionAppDetailsFragmentToDevAppsFragment(app.developerName) .actionAppDetailsFragmentToDevAppsFragment(app.developerName)
) )
} }
txtLine3.text = ("${app.versionName} (${app.versionCode})")
packageName.text = app.packageName
val tags = mutableListOf<String>() // Do not show tags for web apps or unknown apps
if (app.isFree) if (isFullApp) {
tags.add(getString(R.string.details_free)) val tags = mutableSetOf<String>().apply {
else if (app.isFree) {
tags.add(getString(R.string.details_paid)) add(getString(R.string.details_free))
} else {
add(getString(R.string.details_paid))
}
if (app.containsAds) if (app.containsAds) {
tags.add(getString(R.string.details_contains_ads)) add(getString(R.string.details_contains_ads))
else } else {
tags.add(getString(R.string.details_no_ads)) 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
} }
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {} txtLine4.text = tags.joinToString(separator = "")
})
}
private fun updateActionState(state: State) {
runOnUiThread {
binding.layoutDetailsInstall.btnDownload.apply {
updateState(state)
if (state == State.INSTALLING) {
setButtonState(false)
setText(R.string.action_installing)
}
} }
} }
} }
@@ -548,32 +503,15 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
@Synchronized private fun purchase(app: App) {
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)
if (app.fileList.requiresObbDir()) { if (app.fileList.requiresObbDir()) {
if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) { if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) {
viewModel.download(app) viewModel.download(app)
} else { } else {
permissionProvider.request(PermissionType.STORAGE_MANAGER) { 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 { } else {
@@ -581,149 +519,185 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
private fun updateProgress(progress: Int, speed: Long = -1, timeRemaining: Long = -1) { private fun updateProgress(progress: Int) {
runOnUiThread { // No need to update progress if it is already 100% / completed
if (progress == 100) { transformIcon(progress != 100)
binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) if (progress == 100) return
return@runOnUiThread
}
binding.layoutDetailsInstall.apply { binding.layoutDetailsApp.apply {
txtProgressPercent.text = ("${progress}%") progressDownload.progress = progress
progressDownload.apply { progressDownload.isIndeterminate = progress < 1
this.progress = progress }
isIndeterminate = progress < 1 }
}
txtEta.text = CommonUtil.getETAString(requireContext(), timeRemaining) private fun transformIcon(ongoing: Boolean = false) {
txtSpeed.text = CommonUtil.getDownloadSpeedString(requireContext(), speed) 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() { private fun checkAndSetupInstall() {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
runOnUiThread { // Setup primary and secondary action buttons
binding.layoutDetailsInstall.btnDownload.let { btn -> binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
btn.setButtonState(true) binding.layoutDetailsApp.btnPrimaryAction.isEnabled = 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()
)
val installedVersion = if (app.isInstalled) {
PackageUtil.getInstalledVersion(requireContext(), app.packageName) 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) { if ((isUpdatable && hasValidCert) || (isUpdatable && isExtendedUpdateEnabled)) {
binding.layoutDetailsApp.txtLine3.text = binding.layoutDetailsApp.btnPrimaryAction.apply {
("$installedVersion${app.versionName} (${app.versionCode})") text = getString(R.string.action_update)
btn.setText(R.string.action_update) setOnClickListener {
btn.addOnClickListener { if (app.versionCode == 0) {
if (app.versionCode == 0) { toast(R.string.toast_app_unavailable)
toast(R.string.toast_app_unavailable) setText(R.string.status_unavailable)
} else { } else {
startDownload() 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 { } else {
if (downloadStatus in DownloadStatus.running) { purchase(app)
flip(1) }
} else if (app.isFree) { }
btn.setText(R.string.action_install)
} else {
btn.setText(app.price)
}
btn.addOnClickListener { binding.layoutDetailsApp.btnSecondaryAction.apply {
if (!permissionProvider.isGranted(PermissionType.INSTALL_UNKNOWN_APPS)) { text = getString(R.string.title_manual_download)
permissionProvider.request(PermissionType.INSTALL_UNKNOWN_APPS) { setOnClickListener {
if (it) { findNavController().navigate(
btn.setText(R.string.download_metadata) AppDetailsFragmentDirections
startDownload() .actionAppDetailsFragmentToManualDownloadSheet(app)
} )
}
} 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()
}
} }
} }
} }
}
@Synchronized // Lay out the toolbar again
private fun flip(nextView: Int) { binding.toolbar.invalidateMenu()
runOnUiThread {
val displayChild = binding.layoutDetailsInstall.viewFlipper.displayedChild if (app.isInstalled) {
if (displayChild != nextView) { binding.toolbar.menu.apply {
binding.layoutDetailsInstall.viewFlipper.displayedChild = nextView findItem(R.id.action_home_screen)?.isVisible =
if (nextView == 0) checkAndSetupInstall() 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() { private fun updateExtraDetails(app: App) {
if (::app.isInitialized) { binding.viewFlipper.displayedChild = 1
attachHeader()
attachBottomSheet()
attachActions()
}
}
private fun inflateExtraDetails(app: App?) { updateAppDescription(app)
app?.let { updateAppRatingAndReviews(app)
binding.viewFlipper.displayedChild = 1 updateAppDevInfo(app)
inflateAppDescription(app) updateAppPermission(app)
inflateAppRatingAndReviews(app)
inflateAppDevInfo(app)
inflateAppDataSafety(app)
inflateAppPrivacy(app)
inflateAppPermission(app)
if (!authProvider.isAnonymous) { // Allow users to handle beta subscriptions, if logged in by own account.
app.testingProgram?.let { if (!authProvider.isAnonymous) {
if (it.isAvailable && it.isSubscribed) { // Update app name to the testing program name, if subscribed
binding.layoutDetailsApp.txtLine1.text = it.displayName app.testingProgram?.let {
} if (it.isAvailable && it.isSubscribed) {
binding.layoutDetailsApp.txtLine1.text = it.displayName
} }
inflateBetaSubscription(app)
} }
if (Preferences.getBoolean(requireContext(), Preferences.PREFERENCE_SIMILAR)) { updateBetaSubscription(app)
inflateAppStream(app)
}
} }
if (showSimilarApps) {
updateAppStream(app)
}
checkAndSetupInstall()
} }
private fun inflateAppDescription(app: App) { private fun updateAppDescription(app: App) {
binding.layoutDetailDescription.apply { binding.layoutDetailDescription.apply {
val installs = CommonUtil.addDiPrefix(app.installs) val installs = CommonUtil.addDiPrefix(app.installs)
@@ -780,10 +754,16 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
private fun inflateAppRatingAndReviews(app: App) { private fun updateAppRatingAndReviews(app: App) {
binding.layoutDetailsReview.apply { binding.layoutDetailsReview.apply {
averageRating.text = app.rating.average.toString() headerRatingReviews.addClickListener {
txtReviewCount.text = app.rating.abbreviatedLabel findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsReviewFragment(
app.displayName,
app.packageName
)
)
}
var totalStars = 0L var totalStars = 0L
totalStars += app.rating.oneStar totalStars += app.rating.oneStar
@@ -803,42 +783,34 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
averageRating.text = String.format(Locale.getDefault(), "%.1f", app.rating.average) averageRating.text = String.format(Locale.getDefault(), "%.1f", app.rating.average)
txtReviewCount.text = app.rating.abbreviatedLabel txtReviewCount.text = app.rating.abbreviatedLabel
}
}
layoutUserReview.visibility = private fun updateUserReview(review: Review) {
if (authProvider.isAnonymous) View.GONE else View.VISIBLE 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 && app.isInstalled) {
if (authProvider.isAnonymous) { btnPostReview.setOnClickListener {
toast(R.string.toast_anonymous_restriction) addOrUpdateReview(
} else { app,
addOrUpdateReview(app, Review().apply { Review().apply {
title = inputTitle.text.toString() title = inputTitle.text.toString()
rating = userStars.rating.toInt() rating = userStars.rating.toInt()
comment = inputReview.text.toString() comment = inputReview.text.toString()
}) }
}
}
headerRatingReviews.addClickListener {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsReviewFragment(
app.displayName,
app.packageName
) )
) }
} else {
layoutUserReview.visibility = View.GONE
} }
} }
} }
private fun inflateAppDataSafety(app: App) { private fun updateAppDevInfo(app: App) {
viewModel.fetchAppDataSafetyReport(app.packageName)
}
private fun inflateAppPrivacy(app: App) {
viewModel.fetchAppReport(app.packageName)
}
private fun inflateAppDevInfo(app: App) {
binding.layoutDetailsDev.apply { binding.layoutDetailsDev.apply {
if (app.developerAddress.isNotEmpty()) { if (app.developerAddress.isNotEmpty()) {
devAddress.apply { devAddress.apply {
@@ -868,7 +840,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
private fun inflateBetaSubscription(app: App) { private fun updateBetaSubscription(app: App) {
binding.layoutDetailsBeta.apply { binding.layoutDetailsBeta.apply {
app.testingProgram?.let { betaProgram -> app.testingProgram?.let { betaProgram ->
if (betaProgram.isAvailable) { if (betaProgram.isAvailable) {
@@ -899,7 +871,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
private fun inflateAppStream(app: App) { private fun updateAppStream(app: App) {
app.detailsStreamUrl?.let { app.detailsStreamUrl?.let {
val carouselController = val carouselController =
DetailsCarouselController(object : GenericCarouselController.Callbacks { DetailsCarouselController(object : GenericCarouselController.Callbacks {
@@ -946,7 +918,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
private fun inflateAppPermission(app: App) { private fun updateAppPermission(app: App) {
binding.layoutDetailsPermissions.apply { binding.layoutDetailsPermissions.apply {
headerPermission.addClickListener { headerPermission.addClickListener {
if (app.permissions.isNotEmpty()) { if (app.permissions.isNotEmpty()) {
@@ -999,6 +971,13 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
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 */ /* App Review Helpers */
private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout { private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout {

View File

@@ -21,7 +21,6 @@ package com.aurora.store.view.ui.details
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.aurora.Constants import com.aurora.Constants
@@ -49,9 +48,8 @@ class DetailsExodusFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Toolbar // Toolbar
binding.layoutToolbarNative.toolbar.apply { binding.toolbar.apply {
title = args.displayName title = args.displayName
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
} }

View File

@@ -60,9 +60,7 @@ class DetailsMoreFragment : BaseFragment<FragmentDetailsMoreBinding>() {
} }
// Toolbar // Toolbar
binding.layoutToolbarActionMore.toolbar.setOnClickListener { binding.toolbar.setOnClickListener { findNavController().navigateUp() }
findNavController().navigateUp()
}
inflateDescription(args.app) inflateDescription(args.app)
inflateFiles(args.app) inflateFiles(args.app)
@@ -95,7 +93,7 @@ class DetailsMoreFragment : BaseFragment<FragmentDetailsMoreBinding>() {
} }
private fun inflateDescription(app: App) { private fun inflateDescription(app: App) {
binding.layoutToolbarActionMore.txtTitle.text = app.displayName binding.toolbar.title = app.displayName
binding.txtDescription.text = HtmlCompat.fromHtml( binding.txtDescription.text = HtmlCompat.fromHtml(
app.description, app.description,
HtmlCompat.FROM_HTML_MODE_COMPACT HtmlCompat.FROM_HTML_MODE_COMPACT

View File

@@ -58,11 +58,9 @@ class DetailsReviewFragment : BaseFragment<FragmentDetailsReviewBinding>() {
} }
// Toolbar // Toolbar
binding.layoutToolbarActionReview.apply { binding.toolbar.apply {
txtTitle.text = args.displayName title = args.displayName
toolbar.setOnClickListener { setNavigationOnClickListener { findNavController().navigateUp() }
findNavController().navigateUp()
}
} }
viewModel.liveData.observe(viewLifecycleOwner) { viewModel.liveData.observe(viewLifecycleOwner) {
@@ -91,6 +89,7 @@ class DetailsReviewFragment : BaseFragment<FragmentDetailsReviewBinding>() {
binding.chipGroup.setOnCheckedStateChangeListener { _, checkedIds -> binding.chipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
when (checkedIds[0]) { when (checkedIds[0]) {
R.id.filter_review_all -> filter = Review.Filter.ALL 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_critical -> filter = Review.Filter.CRITICAL
R.id.filter_review_positive -> filter = Review.Filter.POSITIVE R.id.filter_review_positive -> filter = Review.Filter.POSITIVE
R.id.filter_review_five -> filter = Review.Filter.FIVE R.id.filter_review_five -> filter = Review.Filter.FIVE

View File

@@ -21,12 +21,10 @@ package com.aurora.store.view.ui.details
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.aurora.gplayapi.data.models.SearchBundle import com.aurora.gplayapi.data.models.SearchBundle
import com.aurora.store.R
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppProgressViewModel_ import com.aurora.store.view.epoxy.views.AppProgressViewModel_
@@ -49,9 +47,8 @@ class DevAppsFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
} }
// Toolbar // Toolbar
binding.layoutToolbarNative.toolbar.apply { binding.toolbar.apply {
title = args.developerName title = args.developerName
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
} }

View File

@@ -50,10 +50,9 @@ class DevProfileFragment : BaseFragment<FragmentDevProfileBinding>(),
val developerCarouselController = DeveloperCarouselController(this) val developerCarouselController = DeveloperCarouselController(this)
// Toolbar // Toolbar
binding.layoutToolbarAction.apply { binding.toolbar.apply {
txtTitle.text = title = if (args.title.isNullOrBlank()) getString(R.string.details_dev_profile) else args.title
if (args.title.isNullOrBlank()) getString(R.string.details_dev_profile) else args.title setNavigationOnClickListener { findNavController().navigateUp() }
toolbar.setOnClickListener { findNavController().navigateUp() }
} }
// RecyclerView // RecyclerView
@@ -78,7 +77,7 @@ class DevProfileFragment : BaseFragment<FragmentDevProfileBinding>(),
is ViewState.Success<*> -> { is ViewState.Success<*> -> {
(it.data as DevStream).apply { (it.data as DevStream).apply {
binding.layoutToolbarAction.txtTitle.text = title binding.toolbar.title = title
binding.txtDevName.text = title binding.txtDevName.text = title
binding.txtDevDescription.text = description binding.txtDevDescription.text = description
binding.imgIcon.load(imgUrl) binding.imgIcon.load(imgUrl)

View File

@@ -22,7 +22,6 @@ package com.aurora.store.view.ui.downloads
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.aurora.Constants.GITLAB_URL import com.aurora.Constants.GITLAB_URL
@@ -54,11 +53,7 @@ class DownloadFragment : BaseFragment<FragmentDownloadBinding>() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Toolbar // Toolbar
binding.layoutToolbarAction.toolbar.apply { binding.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)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {

View File

@@ -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.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.search.SearchResultViewModel import com.aurora.store.viewmodel.search.SearchResultViewModel
import com.google.android.material.textfield.TextInputEditText
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
@@ -59,8 +58,6 @@ class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
private val viewModel: SearchResultViewModel by viewModels() private val viewModel: SearchResultViewModel by viewModels()
private lateinit var searchView: TextInputEditText
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private var query: String? = null private var query: String? = null
@@ -84,20 +81,20 @@ class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
sharedPreferences.registerOnSharedPreferenceChangeListener(this) sharedPreferences.registerOnSharedPreferenceChangeListener(this)
// Toolbar // Toolbar
binding.layoutViewToolbar.apply { binding.toolbar.apply {
searchView = inputSearch setNavigationOnClickListener {
imgActionPrimary.setOnClickListener { binding.searchBar.hideKeyboard()
findNavController().navigateUp() findNavController().navigateUp()
} }
imgActionSecondary.setOnClickListener { setOnMenuItemClickListener {
findNavController().navigate(R.id.downloadFragment) when (it.itemId) {
} R.id.action_clear -> {
clearButton.apply { binding.searchBar.text?.clear()
visibility = if (query.isNullOrBlank()) View.GONE else View.VISIBLE binding.searchBar.showKeyboard()
setOnClickListener { }
searchView.text?.clear() R.id.action_download -> findNavController().navigate(R.id.downloadFragment)
searchView.showKeyboard()
} }
true
} }
} }
@@ -186,30 +183,29 @@ class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
} }
} }
} else { } else {
binding.recycler binding.recycler.withModels {
.withModels { setFilterDuplicates(true)
setFilterDuplicates(true)
filteredAppList.forEach { app -> filteredAppList.forEach { app ->
add( add(
AppListViewModel_() AppListViewModel_()
.id(app.id) .id(app.id)
.app(app) .app(app)
.click(View.OnClickListener { .click(View.OnClickListener {
searchView.hideKeyboard() binding.searchBar.hideKeyboard()
openDetailsFragment(app.packageName, app) openDetailsFragment(app.packageName, app)
}) })
) )
}
if (searchBundle.subBundles.isNotEmpty()) {
add(
AppProgressViewModel_()
.id("progress")
)
}
} }
if (searchBundle.subBundles.isNotEmpty()) {
add(
AppProgressViewModel_()
.id("progress")
)
}
}
binding.recycler.adapter?.let { binding.recycler.adapter?.let {
if (it.itemCount < 10) { if (it.itemCount < 10) {
viewModel.next(searchBundle.subBundles) viewModel.next(searchBundle.subBundles)
@@ -219,28 +215,22 @@ class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
} }
private fun attachSearch() { private fun attachSearch() {
searchView.addTextChangedListener(object : TextWatcher { binding.searchBar.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.isNotEmpty()) { binding.toolbar.menu.findItem(R.id.action_clear)?.isVisible = s.isNotBlank()
binding.layoutViewToolbar.clearButton.visibility = View.VISIBLE
} else {
binding.layoutViewToolbar.clearButton.visibility = View.GONE
}
} }
override fun afterTextChanged(s: Editable) {} override fun afterTextChanged(s: Editable) {}
}) })
searchView.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> binding.searchBar.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? ->
if (actionId == EditorInfo.IME_ACTION_SEARCH if (actionId == EditorInfo.IME_ACTION_SEARCH
|| actionId == KeyEvent.ACTION_DOWN || actionId == KeyEvent.ACTION_DOWN
|| actionId == KeyEvent.KEYCODE_ENTER || actionId == KeyEvent.KEYCODE_ENTER
) { ) {
query = searchView.text.toString() query = binding.searchBar.text.toString()
query?.let { query?.let {
requireArguments().putString("query", it) requireArguments().putString("query", it)
queryViewModel(it) queryViewModel(it)
@@ -252,8 +242,8 @@ class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
} }
private fun updateQuery(query: String) { private fun updateQuery(query: String) {
searchView.text = Editable.Factory.getInstance().newEditable(query) binding.searchBar.text = Editable.Factory.getInstance().newEditable(query)
searchView.setSelection(query.length) binding.searchBar.setSelection(query.length)
queryViewModel(query) queryViewModel(query)
} }
@@ -277,7 +267,6 @@ class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
.toList() .toList()
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == PREFERENCE_FILTER) query?.let { queryViewModel(it) } if (key == PREFERENCE_FILTER) query?.let { queryViewModel(it) }
} }

View File

@@ -37,7 +37,6 @@ import com.aurora.store.databinding.FragmentSearchSuggestionBinding
import com.aurora.store.view.epoxy.views.SearchSuggestionViewModel_ import com.aurora.store.view.epoxy.views.SearchSuggestionViewModel_
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.search.SearchSuggestionViewModel import com.aurora.store.viewmodel.search.SearchSuggestionViewModel
import com.google.android.material.textfield.TextInputEditText
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -47,26 +46,21 @@ class SearchSuggestionFragment : BaseFragment<FragmentSearchSuggestionBinding>()
private val viewModel: SearchSuggestionViewModel by viewModels() private val viewModel: SearchSuggestionViewModel by viewModels()
private lateinit var searchView: TextInputEditText
private var query: String = String()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Toolbar // Toolbar
binding.layoutToolbarSearch.apply { binding.toolbar.apply {
searchView = inputSearch setNavigationOnClickListener {
imgActionPrimary.setOnClickListener { binding.searchBar.hideKeyboard()
searchView.hideKeyboard()
findNavController().navigateUp() findNavController().navigateUp()
} }
imgActionSecondary.setOnClickListener { setOnMenuItemClickListener {
findNavController().navigate(R.id.downloadFragment) when (it.itemId) {
} R.id.action_clear -> binding.searchBar.text?.clear()
clearButton.apply { R.id.action_download -> findNavController().navigate(R.id.downloadFragment)
visibility = if (query.isBlank()) View.GONE else View.VISIBLE }
setOnClickListener { searchView.text?.clear() } true
} }
} }
@@ -81,9 +75,7 @@ class SearchSuggestionFragment : BaseFragment<FragmentSearchSuggestionBinding>()
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (::searchView.isInitialized) { binding.searchBar.showKeyboard()
searchView.showKeyboard()
}
} }
private fun updateController(searchSuggestions: List<SearchSuggestEntry>) { private fun updateController(searchSuggestions: List<SearchSuggestEntry>) {
@@ -98,7 +90,7 @@ class SearchSuggestionFragment : BaseFragment<FragmentSearchSuggestionBinding>()
updateQuery(it.title) updateQuery(it.title)
} }
.click { _ -> .click { _ ->
searchView.hideKeyboard() binding.searchBar.hideKeyboard()
search(it.title) search(it.title)
} }
) )
@@ -107,32 +99,30 @@ class SearchSuggestionFragment : BaseFragment<FragmentSearchSuggestionBinding>()
} }
private fun setupSearch() { 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 beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.isNotEmpty()) { if (s.isNotEmpty()) {
query = s.toString() val query = s.toString()
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
viewModel.observeStreamBundles(query) 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) {} override fun afterTextChanged(s: Editable) {}
}) })
searchView.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> binding.searchBar.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? ->
if (actionId == EditorInfo.IME_ACTION_SEARCH if (actionId == EditorInfo.IME_ACTION_SEARCH
|| actionId == KeyEvent.ACTION_DOWN || actionId == KeyEvent.ACTION_DOWN
|| actionId == KeyEvent.KEYCODE_ENTER || actionId == KeyEvent.KEYCODE_ENTER
) { ) {
query = searchView.text.toString() val query = binding.searchBar.text.toString()
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
searchView.hideKeyboard() binding.searchBar.hideKeyboard()
search(query) search(query)
return@setOnEditorActionListener true return@setOnEditorActionListener true
} }
@@ -142,8 +132,8 @@ class SearchSuggestionFragment : BaseFragment<FragmentSearchSuggestionBinding>()
} }
private fun updateQuery(query: String) { private fun updateQuery(query: String) {
searchView.text = Editable.Factory.getInstance().newEditable(query) binding.searchBar.text = Editable.Factory.getInstance().newEditable(query)
searchView.setSelection(query.length) binding.searchBar.setSelection(query.length)
} }
private fun search(query: String) { private fun search(query: String) {

View File

@@ -19,19 +19,35 @@
package com.aurora.store.view.ui.splash package com.aurora.store.view.ui.splash
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Intent import android.content.Intent
import android.net.UrlQuerySanitizer import android.net.UrlQuerySanitizer
import android.os.Bundle 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 android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.aurora.extensions.hide import com.aurora.extensions.hide
import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.runOnUiThread
import com.aurora.extensions.show import com.aurora.extensions.show
import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.AuthState import com.aurora.store.data.model.AuthState
import com.aurora.store.databinding.FragmentSplashBinding 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
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_INTRO import com.aurora.store.util.Preferences.PREFERENCE_INTRO
@@ -44,8 +60,20 @@ import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class SplashFragment : BaseFragment<FragmentSplashBinding>() { class SplashFragment : BaseFragment<FragmentSplashBinding>() {
private val TAG = SplashFragment::class.java.simpleName
private val viewModel: AuthViewModel by activityViewModels() 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -57,9 +85,7 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
} }
// Toolbar // Toolbar
binding.layoutToolbarAction.toolbar.apply { binding.toolbar.apply {
elevation = 0f
inflateMenu(R.menu.menu_splash)
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_blacklist_manager -> { R.id.menu_blacklist_manager -> {
@@ -158,10 +184,10 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
private fun updateActionLayout(isVisible: Boolean) { private fun updateActionLayout(isVisible: Boolean) {
if (isVisible) { if (isVisible) {
binding.layoutAction.show() binding.layoutAction.show()
binding.layoutToolbarAction.toolbar.visibility = View.VISIBLE binding.toolbar.visibility = View.VISIBLE
} else { } else {
binding.layoutAction.hide() binding.layoutAction.hide()
binding.layoutToolbarAction.toolbar.visibility = View.GONE binding.toolbar.visibility = View.GONE
} }
} }
@@ -176,7 +202,29 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
binding.btnGoogle.addOnClickListener { binding.btnGoogle.addOnClickListener {
if (viewModel.authState.value != AuthState.Fetching) { if (viewModel.authState.value != AuthState.Fetching) {
binding.btnGoogle.updateProgress(true) 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<FragmentSplashBinding>() {
} }
private fun navigateToDefaultTab() { private fun navigateToDefaultTab() {
val defaultDestination = Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB) val defaultDestination =
Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB)
val directions = val directions =
when (requireArguments().getInt("destinationId", defaultDestination)) { when (requireArguments().getInt("destinationId", defaultDestination)) {
R.id.updatesFragment -> { R.id.updatesFragment -> {
@@ -206,19 +255,49 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
2 -> SplashFragmentDirections.actionSplashFragmentToUpdatesFragment() 2 -> SplashFragmentDirections.actionSplashFragmentToUpdatesFragment()
else -> SplashFragmentDirections.actionSplashFragmentToNavigationApps() 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) findNavController().navigate(directions)
} }
private fun getPackageName(): String { private fun getPackageName(): String {
// Navigation component cannot handle market scheme as its missing a valid host // 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") ?: "" requireActivity().intent.data!!.getQueryParameter("id") ?: ""
} else if (activity?.intent != null && activity?.intent?.action == Intent.ACTION_SEND) { } else if (requireActivity().intent != null && requireActivity().intent.action == Intent.ACTION_SEND) {
val clipData = activity?.intent?.getStringExtra(Intent.EXTRA_TEXT) ?: "" val clipData = requireActivity().intent.getStringExtra(Intent.EXTRA_TEXT) ?: ""
UrlQuerySanitizer(clipData).getValue("id") ?: "" UrlQuerySanitizer(clipData).getValue("id") ?: ""
} else { } else {
requireArguments().getString("packageName") ?: "" requireArguments().getString("packageName") ?: ""
} }
} }
private fun fetchGoogleAccounts(): Array<Account> {
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")
}
}
} }

View File

@@ -25,7 +25,6 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -34,7 +33,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.providers.NativeDeviceInfoProvider 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.util.PathUtil
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
@@ -42,7 +41,7 @@ import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class SpoofFragment : BaseFragment<FragmentGenericWithPagerBinding>() { class SpoofFragment : BaseFragment<FragmentSpoofBinding>() {
private val TAG = SpoofFragment::class.java.simpleName private val TAG = SpoofFragment::class.java.simpleName
// Android is weird, even if export device config with proper mime type, it will refuse to open // Android is weird, even if export device config with proper mime type, it will refuse to open
@@ -63,11 +62,7 @@ class SpoofFragment : BaseFragment<FragmentGenericWithPagerBinding>() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Toolbar // Toolbar
binding.layoutActionToolbar.toolbar.apply { binding.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)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {

View File

@@ -21,11 +21,14 @@ package com.aurora.store.viewmodel.all
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.store.data.providers.BlacklistProvider import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.PackageUtil 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.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -37,6 +40,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BlacklistViewModel @Inject constructor( class BlacklistViewModel @Inject constructor(
val blacklistProvider: BlacklistProvider, val blacklistProvider: BlacklistProvider,
val gson: Gson,
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) : ViewModel() { ) : 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<String> = gson.fromJson(
it.bufferedReader().readText(),
object : TypeToken<MutableSet<String?>?>() {}.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)
}
}
}
} }

View File

@@ -21,6 +21,7 @@ package com.aurora.store.viewmodel.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.store.data.room.favourite.Favourite import com.aurora.store.data.room.favourite.Favourite
@@ -39,26 +40,38 @@ class FavouriteViewModel @Inject constructor(
private val favouriteDao: FavouriteDao, private val favouriteDao: FavouriteDao,
private val gson: Gson private val gson: Gson
) : ViewModel() { ) : ViewModel() {
private val TAG = FavouriteViewModel::class.java.simpleName
val favouritesList = favouriteDao.favourites() val favouritesList = favouriteDao.favourites()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun importFavourites(context: Context, uri: Uri) { fun importFavourites(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
context.contentResolver.openInputStream(uri)?.use { try {
val importExport = context.contentResolver.openInputStream(uri)?.use {
gson.fromJson(it.bufferedReader().readText(), ImportExport::class.java) val importExport = gson.fromJson(
favouriteDao.insertAll( it.bufferedReader().readText(),
importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) } 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) { fun exportFavourites(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
context.contentResolver.openOutputStream(uri)?.use { try {
it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray()) context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray())
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to export favourites", exception)
} }
} }
} }

View File

@@ -20,11 +20,17 @@
package com.aurora.store.viewmodel.all package com.aurora.store.viewmodel.all
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.aurora.store.util.PackageUtil
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -35,13 +41,16 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class InstalledViewModel @Inject constructor( 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() { ) : ViewModel() {
private val TAG = InstalledViewModel::class.java.simpleName private val TAG = InstalledViewModel::class.java.simpleName
private val _packages = MutableStateFlow<List<PackageInfo>?>(null) private val _apps = MutableStateFlow<List<App>?>(null)
val packages = _packages.asStateFlow() val apps = _apps.asStateFlow()
init { init {
fetchApps() fetchApps()
@@ -50,10 +59,35 @@ class InstalledViewModel @Inject constructor(
fun fetchApps() { fun fetchApps() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { 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) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps", exception) Log.e(TAG, "Failed to fetch apps", exception)
} }
} }
} }
fun exportApps(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
try {
val favourites: List<Favourite> = 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)
}
}
}
} }

View File

@@ -1,9 +1,11 @@
package com.aurora.store.viewmodel.details package com.aurora.store.viewmodel.details
import android.content.Context
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.Constants import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.details.TestingProgramStatus 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.ReviewsHelper
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.gplayapi.network.IHttpClient import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.AuroraApp
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.ExodusReport import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.Report import com.aurora.store.data.model.Report
import com.aurora.store.data.room.favourite.Favourite import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.FavouriteDao import com.aurora.store.data.room.favourite.FavouriteDao
import com.aurora.store.util.PackageUtil
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
@@ -32,6 +41,7 @@ import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@HiltViewModel @HiltViewModel
class AppDetailsViewModel @Inject constructor( class AppDetailsViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val appDetailsHelper: AppDetailsHelper, private val appDetailsHelper: AppDetailsHelper,
private val reviewsHelper: ReviewsHelper, private val reviewsHelper: ReviewsHelper,
private val webDataSafetyHelper: WebDataSafetyHelper, private val webDataSafetyHelper: WebDataSafetyHelper,
@@ -69,7 +79,10 @@ class AppDetailsViewModel @Inject constructor(
private val _favourite = MutableStateFlow<Boolean>(false) private val _favourite = MutableStateFlow<Boolean>(false)
val favourite = _favourite.asStateFlow() 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) { fun fetchAppDetails(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -77,7 +90,9 @@ class AppDetailsViewModel @Inject constructor(
checkFavourite(packageName) checkFavourite(packageName)
val app: App = appStash.getOrPut(packageName) { val app: App = appStash.getOrPut(packageName) {
appDetailsHelper.getAppByPackageName(packageName) appDetailsHelper.getAppByPackageName(packageName).apply {
isInstalled = PackageUtil.isInstalled(context, packageName)
}
} }
_app.emit(app) _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) { fun postAppReview(packageName: String, review: Review, isBeta: Boolean) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val userReview = userReviewStash.getOrPut(packageName) { val userReview = reviewsHelper.addOrEditReview(
reviewsHelper.addOrEditReview( packageName,
packageName, review.title,
review.title, review.comment,
review.comment, review.rating,
review.rating, isBeta
isBeta )
)
}
if (userReview != null) { if (userReview != null) {
context.toast(R.string.toast_rated_success)
userReviewStash[packageName] = userReview
_userReview.emit(userReview) _userReview.emit(userReview)
} }
} catch (exception: Exception) { } catch (exception: Exception) {
@@ -206,8 +243,8 @@ class AppDetailsViewModel @Inject constructor(
private fun getLatestExodusReport(packageName: String): Report? { private fun getLatestExodusReport(packageName: String): Report? {
val headers: MutableMap<String, String> = mutableMapOf() val headers: MutableMap<String, String> = mutableMapOf()
headers["Content-Type"] = "application/json" headers["Content-Type"] = Constants.JSON_MIME_TYPE
headers["Accept"] = "application/json" headers["Accept"] = Constants.JSON_MIME_TYPE
headers["Authorization"] = "Token ${BuildConfig.EXODUS_API_KEY}" headers["Authorization"] = "Token ${BuildConfig.EXODUS_API_KEY}"
val url = Constants.EXODUS_SEARCH_URL + packageName val url = Constants.EXODUS_SEARCH_URL + packageName

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners
android:topLeftRadius="14dp"
android:topRightRadius="14dp" />
<solid android:color="?colorPrimary" />
</shape>
</item>
</layer-list>

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="@color/colorRedAlt" />
</shape>

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="15dp" />
<stroke
android:width="1dp"
android:color="?colorControlHighlight" />
</shape>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp" />
</shape>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Disabled -->
<item android:color="@android:color/transparent" android:state_enabled="false"/>
<!-- Selected -->
<item android:color="?attr/colorControlActivated" android:state_selected="true"/>
<item android:color="?attr/colorControlHighlight" android:state_checked="true"/>
<!-- Not selected -->
<item android:color="?android:colorBackground" />
</selector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,160Q80,127 103.5,103.5Q127,80 160,80L480,80L720,320L720,490L640,490L640,360L440,360L440,160L160,160Q160,160 160,160Q160,160 160,160L160,800Q160,800 160,800Q160,800 160,800L600,800L600,880L160,880ZM160,800L160,490L160,490L160,360L160,160L160,160Q160,160 160,160Q160,160 160,160L160,800Q160,800 160,800Q160,800 160,800ZM200,760Q204,711 230,670Q256,629 298,605L260,537Q260,536 264,522Q269,520 273.5,520Q278,520 280,525L319,595Q339,587 359,582.5Q379,578 400,578Q421,578 441,582.5Q461,587 481,595L520,525Q520,525 535,521Q540,523 541,528Q542,533 540,537L502,605Q544,629 570,670Q596,711 600,760L200,760ZM310,700Q318,700 324,694Q330,688 330,680Q330,672 324,666Q318,660 310,660Q302,660 296,666Q290,672 290,680Q290,688 296,694Q302,700 310,700ZM490,700Q498,700 504,694Q510,688 510,680Q510,672 504,666Q498,660 490,660Q482,660 476,666Q470,672 470,680Q470,688 476,694Q482,700 490,700ZM800,880L640,720L696,663L760,726L760,560L840,560L840,726L904,663L960,720L800,880Z"/>
</vector>

View File

@@ -27,9 +27,14 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".view.ui.about.AboutFragment"> tools:context=".view.ui.about.AboutFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action" android:id="@+id/toolbar"
layout="@layout/view_toolbar_action" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/title_about" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -27,9 +27,14 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".view.ui.account.AccountFragment"> tools:context=".view.ui.account.AccountFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action" android:id="@+id/toolbar"
layout="@layout/view_toolbar_action" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/title_account_manager" />
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/chip_layout" android:id="@+id/chip_layout"
@@ -140,4 +145,4 @@
app:btnStateText="@string/action_logout" /> app:btnStateText="@string/action_logout" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -28,9 +28,13 @@
android:weightSum="2" android:weightSum="2"
tools:context=".view.ui.splash.SplashFragment"> tools:context=".view.ui.splash.SplashFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action" android:id="@+id/toolbar"
layout="@layout/view_toolbar_native" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:menu="@menu/menu_splash" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -26,9 +26,14 @@
android:weightSum="3" android:weightSum="3"
tools:context=".view.ui.about.AboutFragment"> tools:context=".view.ui.about.AboutFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action" android:id="@+id/toolbar"
layout="@layout/view_toolbar_action" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/title_about" />
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -28,9 +28,14 @@
android:weightSum="2" android:weightSum="2"
tools:context=".view.ui.account.AccountFragment"> tools:context=".view.ui.account.AccountFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action" android:id="@+id/toolbar"
layout="@layout/view_toolbar_action" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/title_account_manager" />
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -131,4 +136,4 @@
app:btnStateIcon="@drawable/ic_logout" app:btnStateIcon="@drawable/ic_logout"
app:btnStateText="@string/action_logout" /> app:btnStateText="@string/action_logout" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>

View File

@@ -31,9 +31,14 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_details_toolbar" android:id="@+id/toolbar"
layout="@layout/view_toolbar_native" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:menu="@menu/menu_details"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/scrollView" android:id="@+id/scrollView"
@@ -119,8 +124,4 @@
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</LinearLayout> </LinearLayout>
<include
android:id="@+id/layout_details_install"
layout="@layout/layout_details_install" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -25,9 +25,13 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".view.ui.details.DetailsMoreFragment"> tools:context=".view.ui.details.DetailsMoreFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action_more" android:id="@+id/toolbar"
layout="@layout/view_toolbar_action" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -100,4 +104,4 @@
tools:listitem="@layout/view_file" /> tools:listitem="@layout/view_file" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</LinearLayout> </LinearLayout>

View File

@@ -25,9 +25,13 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".view.ui.details.DetailsReviewFragment"> tools:context=".view.ui.details.DetailsReviewFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action_review" android:id="@+id/toolbar"
layout="@layout/view_toolbar_action" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back" />
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/sort_view" android:id="@+id/sort_view"
@@ -53,6 +57,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/filter_review_all" /> android:text="@string/filter_review_all" />
<com.google.android.material.chip.Chip
android:id="@+id/filter_newest_first"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filter_latest" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/filter_review_critical" android:id="@+id/filter_review_critical"
style="@style/Widget.Material3.Chip.Filter" style="@style/Widget.Material3.Chip.Filter"
@@ -117,4 +128,4 @@
app:itemSpacing="@dimen/margin_normal" app:itemSpacing="@dimen/margin_normal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_review" /> tools:listitem="@layout/view_review" />
</LinearLayout> </LinearLayout>

View File

@@ -27,10 +27,13 @@
android:showDividers="middle" android:showDividers="middle"
tools:context=".view.ui.details.DevProfileFragment"> tools:context=".view.ui.details.DevProfileFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action" android:id="@+id/toolbar"
layout="@layout/view_toolbar_action" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back" />
<ViewFlipper <ViewFlipper
android:id="@+id/view_flipper" android:id="@+id/view_flipper"
@@ -98,4 +101,4 @@
android:layout_centerInParent="true" /> android:layout_centerInParent="true" />
</RelativeLayout> </RelativeLayout>
</ViewFlipper> </ViewFlipper>
</LinearLayout> </LinearLayout>

View File

@@ -25,9 +25,15 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".view.ui.downloads.DownloadFragment"> tools:context=".view.ui.downloads.DownloadFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action" android:id="@+id/toolbar"
layout="@layout/view_toolbar_native" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:menu="@menu/menu_download_main"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/title_download_manager" />
<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
@@ -38,4 +44,4 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:stackFromEnd="false" app:stackFromEnd="false"
tools:listitem="@layout/view_download" /> tools:listitem="@layout/view_download" />
</LinearLayout> </LinearLayout>

View File

@@ -24,18 +24,35 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_native" android:id="@+id/toolbar"
layout="@layout/view_toolbar_search" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/searchBar"
android:layout_width="match_parent"
android:layout_height="42dp"
android:background="@null"
android:hint="@string/search_hint"
android:imeOptions="flagNoExtractUi|actionSearch"
android:inputType="text"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_normal"
android:singleLine="true" />
</androidx.appcompat.widget.Toolbar>
<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@+id/layout_toolbar_native" android:layout_below="@+id/toolbar"
android:clipToPadding="true" android:clipToPadding="true"
android:paddingBottom="@dimen/height_bottom_adj" android:paddingBottom="@dimen/height_bottom_adj"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:stackFromEnd="false" app:stackFromEnd="false"
tools:listitem="@layout/view_app_list" /> tools:listitem="@layout/view_app_list" />
</RelativeLayout> </RelativeLayout>

View File

@@ -23,17 +23,21 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_native" android:id="@+id/toolbar"
layout="@layout/view_toolbar_native" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationIcon="@drawable/ic_arrow_back" />
<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@+id/layout_toolbar_native" android:layout_below="@+id/toolbar"
android:clipToPadding="true" android:clipToPadding="true"
android:paddingBottom="@dimen/height_bottom_adj" android:paddingBottom="@dimen/height_bottom_adj"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:stackFromEnd="false" /> app:stackFromEnd="false" />
</RelativeLayout> </RelativeLayout>

View File

@@ -30,15 +30,33 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_view_toolbar" android:id="@+id/toolbar"
layout="@layout/view_toolbar_search" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:menu="@menu/menu_search"
app:navigationIcon="@drawable/ic_arrow_back">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/searchBar"
android:layout_width="match_parent"
android:layout_height="42dp"
android:background="@null"
android:hint="@string/search_hint"
android:imeOptions="flagNoExtractUi|actionSearch"
android:inputType="text"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_normal"
android:singleLine="true" />
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.divider.MaterialDivider <com.google.android.material.divider.MaterialDivider
android:id="@+id/divider" android:id="@+id/divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/layout_view_toolbar" /> android:layout_below="@id/toolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"

View File

@@ -19,6 +19,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator" android:id="@+id/coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -29,15 +30,33 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_search" android:id="@+id/toolbar"
layout="@layout/view_toolbar_search" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:menu="@menu/menu_search"
app:navigationIcon="@drawable/ic_arrow_back">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/searchBar"
android:layout_width="match_parent"
android:layout_height="42dp"
android:background="@null"
android:hint="@string/search_hint"
android:imeOptions="flagNoExtractUi|actionSearch"
android:inputType="text"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_normal"
android:singleLine="true" />
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.divider.MaterialDivider <com.google.android.material.divider.MaterialDivider
android:id="@+id/divider" android:id="@+id/divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/layout_toolbar_search" /> android:layout_below="@id/toolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
@@ -46,4 +65,4 @@
android:layout_below="@+id/divider" android:layout_below="@+id/divider"
tools:listitem="@layout/view_search_suggestion" /> tools:listitem="@layout/view_search_suggestion" />
</RelativeLayout> </RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -28,9 +28,13 @@
android:weightSum="2" android:weightSum="2"
tools:context=".view.ui.splash.SplashFragment"> tools:context=".view.ui.splash.SplashFragment">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_toolbar_action" android:id="@+id/toolbar"
layout="@layout/view_toolbar_native" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:menu="@menu/menu_splash" />
<RelativeLayout <RelativeLayout
android:id="@+id/layout_top" android:id="@+id/layout_top"

View File

@@ -22,15 +22,21 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<include <androidx.appcompat.widget.Toolbar
android:id="@+id/layout_action_toolbar" android:id="@+id/toolbar"
layout="@layout/view_toolbar_native" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:menu="@menu/menu_import_export"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/title_spoof_manager" />
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout" android:id="@+id/tab_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/layout_action_toolbar" android:layout_below="@id/toolbar"
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:tabGravity="fill" app:tabGravity="fill"
app:tabIndicator="@drawable/tab_indicator" app:tabIndicator="@drawable/tab_indicator"
@@ -44,4 +50,4 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@id/tab_layout" /> android:layout_below="@id/tab_layout" />
</RelativeLayout> </RelativeLayout>

View File

@@ -33,8 +33,7 @@
android:id="@+id/top_tab_group" android:id="@+id/top_tab_group"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_normal" android:padding="@dimen/padding_medium"
android:paddingEnd="@dimen/padding_normal"
app:chipSpacingHorizontal="@dimen/margin_small" app:chipSpacingHorizontal="@dimen/margin_small"
app:selectionRequired="true" app:selectionRequired="true"
app:singleLine="true" app:singleLine="true"
@@ -42,14 +41,14 @@
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/tab_top_free" android:id="@+id/tab_top_free"
style="@style/Widget.MaterialComponents.Chip.Choice" style="@style/Chip.TopChart"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/tab_top_free" /> android:text="@string/tab_top_free" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/tab_top_grossing" android:id="@+id/tab_top_grossing"
style="@style/Widget.MaterialComponents.Chip.Choice" style="@style/Chip.TopChart"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/tab_top_grossing" /> android:text="@string/tab_top_grossing" />
@@ -57,14 +56,14 @@
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/tab_trending" android:id="@+id/tab_trending"
style="@style/Widget.MaterialComponents.Chip.Choice" style="@style/Chip.TopChart"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/tab_trending" /> android:text="@string/tab_trending" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/tab_top_paid" android:id="@+id/tab_top_paid"
style="@style/Widget.MaterialComponents.Chip.Choice" style="@style/Chip.TopChart"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/tab_top_paid" /> android:text="@string/tab_top_paid" />

View File

@@ -18,6 +18,7 @@
--> -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -26,18 +27,35 @@
android:paddingEnd="@dimen/padding_small" android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small"> android:paddingBottom="@dimen/padding_small">
<androidx.appcompat.widget.AppCompatImageView <RelativeLayout
android:id="@+id/img_icon" android:id="@+id/img_icon_layout"
android:layout_width="@dimen/icon_size" android:layout_width="@dimen/icon_size_large"
android:layout_height="@dimen/icon_size" android:layout_height="@dimen/icon_size_large">
tools:src="@drawable/bg_placeholder" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_icon"
android:layout_width="@dimen/icon_size_large"
android:layout_height="@dimen/icon_size_large"
tools:src="@drawable/bg_placeholder" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_download"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:visibility="gone"
app:indicatorSize="@dimen/icon_size_large"
app:trackThickness="3dp"
tools:progress="40" />
</RelativeLayout>
<TextView <TextView
android:id="@+id/txt_line1" android:id="@+id/txt_line1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" android:layout_marginStart="@dimen/margin_small"
android:layout_toEndOf="@id/img_icon" android:layout_toEndOf="@id/img_icon_layout"
android:maxLines="2" android:maxLines="2"
android:textAppearance="@style/TextAppearance.Aurora.SubTitle" android:textAppearance="@style/TextAppearance.Aurora.SubTitle"
tools:text="App Name" /> tools:text="App Name" />
@@ -84,4 +102,31 @@
android:layout_alignEnd="@id/txt_line1" android:layout_alignEnd="@id/txt_line1"
android:textAppearance="@style/TextAppearance.Aurora.Line2" android:textAppearance="@style/TextAppearance.Aurora.Line2"
tools:text="Free" /> tools:text="Free" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/txt_line4"
android:layout_marginTop="@dimen/margin_medium"
android:divider="@drawable/divider"
android:orientation="horizontal"
android:showDividers="middle"
android:weightSum="2">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_secondary_action"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="@string/title_manual_download" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_primary_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="@string/action_install" />
</LinearLayout>
</RelativeLayout>

View File

@@ -22,9 +22,11 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="@dimen/padding_small" android:paddingStart="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small" android:paddingEnd="@dimen/padding_small"
android:showDividers="middle"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
@@ -40,7 +42,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/margin_small"
android:orientation="vertical"> android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
@@ -61,11 +62,9 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_beta_Action" android:id="@+id/btn_beta_Action"
android:layout_width="wrap_content" style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:insetTop="@dimen/margin_xsmall"
android:insetBottom="@dimen/margin_xsmall"
android:minWidth="128dp"
android:text="@string/action_join" android:text="@string/action_join"
app:cornerRadius="@dimen/radius_small" /> app:cornerRadius="@dimen/radius_small" />
</LinearLayout> </LinearLayout>

View File

@@ -34,70 +34,52 @@
android:orientation="vertical"> android:orientation="vertical">
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/layout_extras"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none"> android:scrollbars="none">
<LinearLayout <com.google.android.material.chip.ChipGroup
android:id="@+id/layout_extras"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:divider="@drawable/divider_line" android:paddingStart="@dimen/padding_medium"
android:dividerPadding="@dimen/padding_xsmall" android:paddingEnd="@dimen/padding_medium"
android:orientation="horizontal" app:selectionRequired="false"
android:padding="@dimen/padding_small" app:singleLine="true">
android:showDividers="middle">
<androidx.appcompat.widget.AppCompatTextView <com.google.android.material.chip.Chip
android:id="@+id/txt_rating" android:id="@+id/txt_rating"
style="@style/Chip.Tag"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" app:chipIcon="@drawable/ic_star"
android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableStartCompat="@drawable/ic_star"
app:drawableTint="?colorAccent"
tools:text="3.5" /> tools:text="3.5" />
<androidx.appcompat.widget.AppCompatTextView <com.google.android.material.chip.Chip
android:id="@+id/txt_installs" android:id="@+id/txt_installs"
style="@style/Chip.Tag"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" app:chipIcon="@drawable/ic_download_manager"
android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableStartCompat="@drawable/ic_download_manager"
app:drawableTint="?colorAccent"
tools:text="2500" /> tools:text="2500" />
<androidx.appcompat.widget.AppCompatTextView <com.google.android.material.chip.Chip
android:id="@+id/txt_size" android:id="@+id/txt_size"
style="@style/Chip.Tag"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" app:chipIcon="@drawable/ic_apk_install"
android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableTint="?colorAccent"
tools:text="25 MB" /> tools:text="25 MB" />
<androidx.appcompat.widget.AppCompatTextView <com.google.android.material.chip.Chip
android:id="@+id/txt_updated" android:id="@+id/txt_updated"
style="@style/Chip.Tag"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small" app:chipIcon="@drawable/ic_updates"
android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableTint="?colorAccent"
tools:text="Jan 21 2020" /> tools:text="Jan 21 2020" />
</LinearLayout> </com.google.android.material.chip.ChipGroup>
</HorizontalScrollView> </HorizontalScrollView>
<LinearLayout <LinearLayout

View File

@@ -1,137 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_bottomsheet"
android:divider="@drawable/divider"
android:orientation="vertical"
android:paddingStart="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small"
android:showDividers="middle"
app:behavior_hideable="false"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<ViewFlipper
android:id="@+id/view_flipper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateFirstView="true"
android:inAnimation="@anim/fade_in"
android:outAnimation="@anim/fade_out"
tools:ignore="UselessParent">
<com.aurora.store.view.custom.layouts.button.ActionButton
android:id="@+id/btn_download"
android:layout_width="match_parent"
android:layout_height="@dimen/height_button"
android:layout_marginTop="@dimen/margin_xsmall"
android:layout_marginBottom="@dimen/margin_xsmall"
android:text="@string/action_install"
app:btnActionText="@string/action_install"
app:btnActionTextColor="?colorOnPrimary" />
<RelativeLayout
android:id="@+id/progress_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/height_button"
android:layout_marginTop="@dimen/margin_xsmall"
android:layout_marginBottom="@dimen/margin_xsmall"
android:gravity="center_vertical"
android:weightSum="4">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_download"
style="@style/Widget.Material3.LinearProgressIndicator.Legacy"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/img_cancel"
android:background="@drawable/bg_rounded_transparent"
android:indeterminate="true"
android:indeterminateTint="?colorOnPrimary"
app:indicatorColor="?colorControlHighlight"
app:trackColor="?colorPrimary"
app:trackThickness="@dimen/icon_size_small" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toStartOf="@id/img_cancel"
android:gravity="center_vertical"
android:orientation="horizontal"
android:weightSum="3">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_progress_percent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_xsmall"
android:layout_weight="1"
android:paddingStart="@dimen/padding_normal"
android:paddingEnd="@dimen/padding_normal"
android:text="0%"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Aurora.Title"
android:textColor="?colorOnPrimary"
android:textSize="32sp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_small"
android:layout_weight="2"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_speed"
style="@style/TextAppearance.Aurora.Line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/download_speed_estimating"
android:textColor="?colorOnPrimary" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_eta"
style="@style/TextAppearance.Aurora.Line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/download_eta_calculating"
android:textColor="?colorOnPrimary" />
</LinearLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_cancel"
android:layout_width="@dimen/icon_size_small"
android:layout_height="@dimen/icon_size_small"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/margin_small"
android:background="@drawable/bg_cancel"
android:padding="@dimen/padding_medium"
android:tint="@color/colorWhite"
app:srcCompat="@drawable/ic_cancel" />
</RelativeLayout>
</ViewFlipper>
</LinearLayout>

View File

@@ -19,7 +19,7 @@
~ ~
--> -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/exodus_card" android:id="@+id/exodus_card"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -41,20 +41,14 @@
android:id="@+id/txt_status" android:id="@+id/txt_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/header_privacy"
android:text="@string/exodus_progress" android:text="@string/exodus_progress"
android:textAppearance="@style/TextAppearance.Aurora.Line1" /> android:textAppearance="@style/TextAppearance.Aurora.Line1" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.TonalButton"
android:id="@+id/btn_request_analysis" android:id="@+id/btn_request_analysis"
style="@style/Widget.Material3.Button" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/txt_status"
android:layout_gravity="center_vertical"
android:insetTop="@dimen/padding_small"
android:insetBottom="@dimen/padding_xsmall"
android:text="@string/action_request_analysis" android:text="@string/action_request_analysis"
app:cornerRadius="@dimen/margin_small" /> app:cornerRadius="@dimen/margin_small" />
</LinearLayout>
</RelativeLayout>

View File

@@ -38,8 +38,9 @@
android:id="@+id/layout_user_review" android:id="@+id/layout_user_review"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:orientation="vertical"
android:orientation="vertical"> android:visibility="gone"
tools:visibility="visible">
<RatingBar <RatingBar
android:id="@+id/user_stars" android:id="@+id/user_stars"
@@ -55,38 +56,39 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginBottom="@dimen/margin_normal"
android:divider="@drawable/divider" android:divider="@drawable/divider"
android:orientation="vertical" android:orientation="vertical"
android:showDividers="middle"> android:showDividers="middle">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_title" style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="42dp" android:layout_height="wrap_content">
android:background="@drawable/bg_changelog"
android:gravity="center_vertical|center_horizontal"
android:hint="@string/details_ratings_title_hint"
android:imeOptions="flagNoExtractUi|actionDone"
android:inputType="text"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_normal"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Aurora.Line2" />
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_review" android:id="@+id/input_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:hint="@string/details_ratings_title_hint"
android:inputType="text"
android:textAppearance="@style/TextAppearance.Aurora.Line2" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="42dp" android:layout_height="wrap_content">
android:background="@drawable/bg_changelog"
android:gravity="center_vertical|center_horizontal" <com.google.android.material.textfield.TextInputEditText
android:hint="@string/details_think_this_app" android:id="@+id/input_review"
android:imeOptions="flagNoExtractUi|actionDone" android:layout_width="match_parent"
android:inputType="text" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_large" android:focusable="true"
android:paddingEnd="@dimen/padding_normal" android:hint="@string/details_think_this_app"
android:singleLine="true" android:inputType="textMultiLine"
android:textAppearance="@style/TextAppearance.Aurora.Line2" /> android:textAppearance="@style/TextAppearance.Aurora.Line2" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_post_review" android:id="@+id/btn_post_review"
@@ -94,7 +96,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:text="@string/action_post" android:text="@string/action_post"
app:cornerRadius="@dimen/margin_small" /> app:cornerRadius="@dimen/radius_small" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -1,32 +0,0 @@
<!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/layout_toolbar_action"
layout="@layout/view_toolbar_native" />
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -87,21 +87,21 @@
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/filter_gfs" android:id="@+id/filter_gfs"
style="@style/AppTheme.FilterChip" style="@style/Chip.Filter"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/action_filter_gsf_dependent_apps" /> android:text="@string/action_filter_gsf_dependent_apps" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/filter_paid" android:id="@+id/filter_paid"
style="@style/AppTheme.FilterChip" style="@style/Chip.Filter"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/action_filter_paid_apps" /> android:text="@string/action_filter_paid_apps" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/filter_ads" android:id="@+id/filter_ads"
style="@style/AppTheme.FilterChip" style="@style/Chip.Filter"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/action_filter_apps_with_ads" /> android:text="@string/action_filter_apps_with_ads" />

View File

@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/height_button">
<ViewFlipper
android:id="@+id/view_flipper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateFirstView="true"
android:inAnimation="@anim/fade_in"
android:outAnimation="@anim/fade_out"
tools:ignore="UselessParent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn"
style="@style/Widget.Material3.Button.TextButton.Dialog.Flush"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.Aurora.SubTitle"
app:iconPadding="@dimen/padding_large"
tools:text="Install" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:srcCompat="@drawable/ic_check"
app:tint="@color/colorWhite" />
</RelativeLayout>
</ViewFlipper>
</RelativeLayout>

View File

@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:paddingStart="@dimen/padding_medium"
android:paddingEnd="@dimen/padding_medium">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_action_primary"
android:layout_width="@dimen/icon_size_default"
android:layout_height="@dimen/icon_size_default"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/margin_small"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/action_back"
app:srcCompat="@drawable/ic_arrow_back"
app:tint="?colorControlNormal" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_title"
style="@style/AuroraTextStyle.Subtitle.Alt"
android:layout_width="match_parent"
android:layout_height="42dp"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/img_action_primary"
android:gravity="center_vertical"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_normal"
android:singleLine="true"
tools:text="Title" />
</RelativeLayout>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
app:layout_scrollFlags="enterAlways|exitUntilCollapsed" />
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_large">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_action_primary"
android:layout_width="@dimen/icon_size_default"
android:layout_height="@dimen/icon_size_default"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/margin_small"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/action_back"
app:srcCompat="@drawable/ic_arrow_back"
app:tint="?colorControlNormal" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_search"
android:layout_width="match_parent"
android:layout_height="42dp"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/clearButton"
android:layout_toEndOf="@id/img_action_primary"
android:background="@null"
android:hint="@string/search_hint"
android:imeOptions="flagNoExtractUi|actionSearch"
android:inputType="text"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_normal"
android:singleLine="true" />
<com.google.android.material.button.MaterialButton
android:id="@+id/clearButton"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/img_action_secondary"
android:contentDescription="@string/details_changelog"
android:visibility="gone"
app:icon="@drawable/ic_cancel"
app:iconTint="?attr/colorControlNormal" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_action_secondary"
android:layout_width="@dimen/icon_size_default"
android:layout_height="@dimen/icon_size_default"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/margin_small"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/title_download_manager"
app:srcCompat="@drawable/ic_arrow_download"
app:tint="?colorControlNormal" />
</RelativeLayout>

View File

@@ -17,11 +17,17 @@
~ ~
--> -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item
<shape android:shape="rectangle"> android:id="@+id/action_select_all"
<corners android:radius="@dimen/radius_large" /> android:title="@string/action_select_all" />
<solid android:color="?android:colorBackground" /> <item
</shape> android:id="@+id/action_remove_all"
</item> android:title="@string/action_remove_all" />
</layer-list> <item
android:id="@+id/action_import"
android:title="@string/action_import" />
<item
android:id="@+id/action_export"
android:title="@string/action_export" />
</menu>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_clear"
android:icon="@drawable/ic_cancel"
android:title="@string/action_clear"
android:visible="false"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_download"
android:icon="@drawable/ic_download_manager"
android:title="@string/title_download_manager"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -71,12 +71,12 @@
android:id="@+id/appsGamesFragment" android:id="@+id/appsGamesFragment"
android:name="com.aurora.store.view.ui.all.AppsGamesFragment" android:name="com.aurora.store.view.ui.all.AppsGamesFragment"
android:label="@string/title_apps_games" android:label="@string/title_apps_games"
tools:layout="@layout/fragment_generic_with_pager" /> tools:layout="@layout/fragment_generic_with_search" />
<fragment <fragment
android:id="@+id/spoofFragment" android:id="@+id/spoofFragment"
android:name="com.aurora.store.view.ui.spoof.SpoofFragment" android:name="com.aurora.store.view.ui.spoof.SpoofFragment"
android:label="@string/title_spoof_manager" android:label="@string/title_spoof_manager"
tools:layout="@layout/fragment_generic_with_pager" /> tools:layout="@layout/fragment_spoof" />
<fragment <fragment
android:id="@+id/favouriteFragment" android:id="@+id/favouriteFragment"
android:name="com.aurora.store.view.ui.commons.FavouriteFragment" android:name="com.aurora.store.view.ui.commons.FavouriteFragment"

View File

@@ -2,7 +2,7 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.Material3.DynamicColors.Dark.NoActionBar"> <style name="AppTheme" parent="Theme.Material3.DynamicColors.Dark.NoActionBar">
<item name="chipStyle">@style/AppTheme.FilterChip</item> <item name="chipStyle">@style/Chip.Filter</item>
<item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item>
<item name="android:statusBarColor">@color/colorTransparent</item> <item name="android:statusBarColor">@color/colorTransparent</item>
</style> </style>

View File

@@ -28,12 +28,6 @@
<attr name="btnStateIcon" format="reference" /> <attr name="btnStateIcon" format="reference" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ActionButton">
<attr name="btnActionText" format="string" />
<attr name="btnActionTextColor" format="string" />
<attr name="btnActionIcon" format="string" />
</declare-styleable>
<declare-styleable name="AuroraProgressView"> <declare-styleable name="AuroraProgressView">
<attr name="minWidth" format="dimension" /> <attr name="minWidth" format="dimension" />
<attr name="maxWidth" format="dimension" /> <attr name="maxWidth" format="dimension" />
@@ -48,4 +42,4 @@
<attr name="txtSubtitle" format="string" /> <attr name="txtSubtitle" format="string" />
<attr name="imgIcon" format="reference" /> <attr name="imgIcon" format="reference" />
</declare-styleable> </declare-styleable>
</resources> </resources>

View File

@@ -85,10 +85,12 @@
<string name="action_pause">"Pause"</string> <string name="action_pause">"Pause"</string>
<string name="action_pending">"Pending"</string> <string name="action_pending">"Pending"</string>
<string name="action_post">"Post"</string> <string name="action_post">"Post"</string>
<string name="action_remove_all">"Remove all"</string>
<string name="action_request_analysis">Request new analysis</string> <string name="action_request_analysis">Request new analysis</string>
<string name="action_restart">"Restart"</string> <string name="action_restart">"Restart"</string>
<string name="action_resume">"Resume"</string> <string name="action_resume">"Resume"</string>
<string name="action_search">"Search"</string> <string name="action_search">"Search"</string>
<string name="action_select_all">"Select all"</string>
<string name="action_share">"Share"</string> <string name="action_share">"Share"</string>
<string name="action_uninstall">"Uninstall"</string> <string name="action_uninstall">"Uninstall"</string>
<string name="action_uninstall_success">"Successfully uninstalled"</string> <string name="action_uninstall_success">"Successfully uninstalled"</string>
@@ -156,6 +158,7 @@
<string name="exodus_substring">known trackers(s) found in</string> <string name="exodus_substring">known trackers(s) found in</string>
<string name="exodus_view_report">"View report"</string> <string name="exodus_view_report">"View report"</string>
<string name="filter_review_all">"All"</string> <string name="filter_review_all">"All"</string>
<string name="filter_latest">"Latest"</string>
<string name="filter_review_critical">"Critical"</string> <string name="filter_review_critical">"Critical"</string>
<string name="filter_review_five">"Five"</string> <string name="filter_review_five">"Five"</string>
<string name="filter_review_four">"Four"</string> <string name="filter_review_four">"Four"</string>
@@ -437,6 +440,12 @@
<string name="toast_fav_import_success">Favourites imported!</string> <string name="toast_fav_import_success">Favourites imported!</string>
<string name="toast_fav_export_success">Favourites exported!</string> <string name="toast_fav_export_success">Favourites exported!</string>
<!-- BlacklistFragment-->
<string name="toast_black_import_failed">Failed to import blacklist!</string>
<string name="toast_black_export_failed">Failed to export blacklist!</string>
<string name="toast_black_import_success">Blacklist imported!</string>
<string name="toast_black_export_success">Blacklist exported!</string>
<!-- ExportWorker --> <!-- ExportWorker -->
<string name="export_app_title">File Exporter</string> <string name="export_app_title">File Exporter</string>
<string name="export_app_summary">Hold on, exporting your file</string> <string name="export_app_summary">Hold on, exporting your file</string>

View File

@@ -20,7 +20,7 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.Material3.DynamicColors.Light.NoActionBar"> <style name="AppTheme" parent="Theme.Material3.DynamicColors.Light.NoActionBar">
<item name="chipStyle">@style/AppTheme.FilterChip</item> <item name="chipStyle">@style/Chip.Filter</item>
<item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item>
<item name="bottomSheetStyle">@style/AppTheme.BottomSheetStyle</item> <item name="bottomSheetStyle">@style/AppTheme.BottomSheetStyle</item>
<item name="android:statusBarColor">@color/colorTransparent</item> <item name="android:statusBarColor">@color/colorTransparent</item>
@@ -47,5 +47,17 @@
<item name="widgetLayout">@layout/preference_material_switch</item> <item name="widgetLayout">@layout/preference_material_switch</item>
</style> </style>
<style name="AppTheme.FilterChip" parent="@style/Widget.Material3.Chip.Filter" /> <style name="Chip.Filter" parent="@style/Widget.Material3.Chip.Filter" />
<style name="Chip.TopChart" parent="@style/Widget.Material3.Chip.Filter">
<item name="android:textAppearance">@style/TextAppearance.Aurora.Line1</item>
<item name="chipStrokeColor">?attr/colorControlHighlight</item>
</style>
<style name="Chip.Tag" parent="@style/Widget.Material3.Chip.Assist">
<item name="android:clickable">false</item>
<item name="android:textAppearance">@style/TextAppearance.Aurora.Line2</item>
<item name="chipBackgroundColor">?attr/colorControlHighlight</item>
<item name="chipStrokeWidth">0dp</item>
</style>
</resources> </resources>

View File

@@ -7,7 +7,7 @@ composeBom = "2024.11.00"
coreVersion = "1.15.0" coreVersion = "1.15.0"
epoxyVersion = "5.1.4" epoxyVersion = "5.1.4"
espressoVersion = "3.6.1" espressoVersion = "3.6.1"
gplayapiVersion = "3.4.3" gplayapiVersion = "3.4.4"
gsonVersion = "2.11.0" gsonVersion = "2.11.0"
hiddenapibypassVersion = "4.3" hiddenapibypassVersion = "4.3"
hiltVersion = "2.53" hiltVersion = "2.53"