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.GET_ACCOUNTS" />
<uses-permission
android:name="android.permission.USE_CREDENTIALS"
android:maxSdkVersion="22" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
@@ -81,7 +87,8 @@
<activity
android:name=".MainActivity"
android:exported="true">
android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<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 UPDATE_URL_STABLE = "https://gitlab.com/AuroraOSS/AuroraStore/raw/master/updates.json"
const val UPDATE_URL_NIGHTLY = "https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json"
const val UPDATE_URL_NIGHTLY =
"https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json"
const val NOTIFICATION_CHANNEL_EXPORT = "NOTIFICATION_CHANNEL_EXPORT"
const val NOTIFICATION_CHANNEL_INSTALL = "NOTIFICATION_CHANNEL_INSTALL"
@@ -56,4 +57,6 @@ object Constants {
const val PAGE_TYPE = "PAGE_TYPE"
const val TOP_CHART_TYPE = "TOP_CHART_TYPE"
const val TOP_CHART_CATEGORY = "TOP_CHART_CATEGORY"
const val JSON_MIME_TYPE = "application/json"
}

View File

@@ -1,14 +1,23 @@
package com.aurora.extensions
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import androidx.core.content.pm.PackageInfoCompat
fun PackageInfo.isValidApp(packageManager: PackageManager): Boolean {
if (this.applicationInfo == null || this.packageName.isEmpty()) return false
// Most core AOSP system apps use their package name as label
if (this.applicationInfo!!.loadLabel(packageManager).startsWith(this.packageName)) return false
// Filter out core AOSP system apps
if (this.applicationInfo!!.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
if (this.packageName.endsWith(".resources")) return false
if (this.applicationInfo!!.loadLabel(packageManager).startsWith(this.packageName)) return false
if (this.versionName?.endsWith("system image") == true) return false
if (this.versionName?.endsWith("-initial") == true) return false
if (this.versionName == Build.VERSION.RELEASE && PackageInfoCompat.getLongVersionCode(this) == Build.VERSION.SDK_INT.toLong()) return false
}
return when {
isQAndAbove -> {

View File

@@ -82,7 +82,7 @@ class MainActivity : AppCompatActivity() {
// Adjust root view's paddings for edgeToEdge display
ViewCompat.setOnApplyWindowInsetsListener(B.root) { root, windowInsets ->
val insets = windowInsets.getInsets(systemBars() or displayCutout() or ime())
root.setPadding(0, insets.top, 0, 0)
root.setPadding(insets.left, insets.top, insets.right, 0)
windowInsets
}

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ package com.aurora.store.data.room.favourite
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Artwork
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -16,6 +18,26 @@ data class Favourite(
val mode: Mode
) : Parcelable {
companion object {
fun fromApp(app: App, mode: Mode): Favourite {
return Favourite(
packageName = app.packageName,
displayName = app.displayName,
iconURL = app.iconArtwork.url,
added = System.currentTimeMillis(),
mode = mode
)
}
fun Favourite.toApp(): App {
return App(
packageName = packageName,
displayName = displayName,
iconArtwork = Artwork(url = iconURL)
)
}
}
enum class Mode {
MANUAL,
IMPORT

View File

@@ -1,7 +1,13 @@
package com.aurora.store.data.work
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import androidx.core.os.bundleOf
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
@@ -10,8 +16,16 @@ import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.store.data.model.AccountType
import com.aurora.store.data.providers.AccountProvider
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.runBlocking
/**
* Worker to refresh [AuthData] in background
@@ -26,6 +40,8 @@ open class AuthWorker @AssistedInject constructor(
private val TAG = AuthWorker::class.java.simpleName
private val authToken: MutableSharedFlow<String?> = MutableSharedFlow(extraBufferCapacity = 1)
override suspend fun doWork(): Result {
if (!AccountProvider.isLoggedIn(appContext)) {
Log.i(TAG, "User has logged out!")
@@ -42,14 +58,48 @@ open class AuthWorker @AssistedInject constructor(
val accountType = AccountProvider.getAccountType(appContext)
val authData = when (accountType) {
AccountType.GOOGLE -> {
authProvider.buildGoogleAuthData(
AccountProvider.getLoginEmail(appContext)!!,
AccountProvider.getLoginToken(appContext)!!.first,
AccountProvider.getLoginToken(appContext)!!.second
).getOrThrow()
val email = AccountProvider.getLoginEmail(appContext)!!
val token = AccountProvider.getLoginToken(appContext)!!.first
val tokenType = AccountProvider.getLoginToken(appContext)!!.second
if (tokenType == AuthHelper.Token.AAS) {
Log.i(TAG, "Refreshing AuthData for personal account")
authProvider.buildGoogleAuthData(email, token, tokenType).getOrThrow()
} else {
/*
* We are working with AuthToken here. The only scenario when we will have
* AuthToken and Google login is when the user used microG to login into
* Aurora Store. In this case, we use system's AccountManager to request credentials.
*/
Log.i(TAG, "Refreshing AuthData for personal account using AccountManager")
AccountManager.get(appContext)
.getAuthToken(
Account(email, GOOGLE_ACCOUNT_TYPE),
GOOGLE_PLAY_AUTH_TOKEN_TYPE,
bundleOf(
"overridePackage" to GOOGLE_PLAY_PACKAGE_NAME,
"overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT)
),
true,
{
authToken.tryEmit(it.result.getString(AccountManager.KEY_AUTHTOKEN))
},
Handler(Looper.getMainLooper())
)
runBlocking {
authProvider.buildGoogleAuthData(
email,
authToken.take(1).first()!!,
tokenType
).getOrThrow()
}
}
}
AccountType.ANONYMOUS -> authProvider.buildAnonymousAuthData().getOrThrow()
AccountType.ANONYMOUS -> {
Log.i(TAG, "Refreshing AuthData for anonymous account")
authProvider.buildAnonymousAuthData().getOrThrow()
}
}
require(verifyAndSaveAuth(authData, accountType) != null)

View File

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

View File

@@ -32,11 +32,27 @@ import com.aurora.store.data.model.Algorithm
import com.aurora.store.util.PackageUtil.getPackageInfo
import java.security.MessageDigest
import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal
object CertUtil {
private val TAG = "CertUtil"
const val GOOGLE_ACCOUNT_TYPE = "com.google"
const val GOOGLE_PLAY_AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay"
const val GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending"
const val GOOGLE_PLAY_CERT =
"MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK"
// Keep this list updated as & when new signatures are added.
private val knownGMSSignatures = listOf(
"bd32424203e0fb25f36b57e5aa356f9bdd1da998",
"38918a453d07199354f8b19af05ec6562ced5788,",
"2169eddb5fbb1fdf241c262681024692c4fc1ecb",
"58e1c4133f7441ec3d2c270270a14802da47ba0e",
"4f87463a1ae6f7d71b2c0b0658845790236dba42"
)
fun isFDroidApp(context: Context, packageName: String): Boolean {
return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName)
}
@@ -84,6 +100,31 @@ object CertUtil {
}
}
fun isGoogleGMS(context: Context, packageName: String): Boolean {
return try {
getX509Certificates(context, packageName).any { certificate ->
val signatureHash = extractSHA1Fingerprint(certificate)
if (knownGMSSignatures.contains(signatureHash)) return true
// Follow heuristics to determine if the app is signed by Google, just to ensure we don't miss any.
listOf(
certificate.issuerX500Principal,
certificate.subjectX500Principal
).any {
val map = parseX500Principal(it)
map["O"] == "Google LLC" || map["O"] == "Google Inc."
&& map["L"] == "Mountain View"
&& map["ST"] == "California"
&& map["C"] == "US"
}
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to check signing cert for $packageName")
false
}
}
private fun isInstalledByFDroid(context: Context, packageName: String): Boolean {
val fdroidPackages = listOf(
"org.fdroid.basic", "org.fdroid.fdroid", "org.fdroid.fdroid.privileged"
@@ -120,4 +161,19 @@ object CertUtil {
getPackageInfo(context, packageName, PackageManager.GET_SIGNATURES)
}
}
private fun extractSHA1Fingerprint(certificate: X509Certificate): String {
val messageDigest = MessageDigest.getInstance(Algorithm.SHA1.value)
messageDigest.update(certificate.encoded)
return messageDigest.digest()
.joinToString("") { byte -> String.format("%02x", byte) }
.lowercase()
}
private fun parseX500Principal(principal: X500Principal): Map<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.SharedLibraryInfo
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.pm.PackageInfoCompat
import androidx.core.graphics.drawable.toBitmap
import com.aurora.extensions.isHuawei
@@ -40,12 +42,17 @@ import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isValidApp
import com.aurora.store.BuildConfig
import com.aurora.store.R
import java.util.Locale
object PackageUtil {
private const val TAG = "PackageUtil"
private const val PACKAGE_NAME_MICRO_G = "com.google.android.gms"
private const val VERSION_CODE_MICRO_G = 240913402
private const val VERSION_CODE_MICRO_G_HUAWEI = 240913007
fun getAllValidPackages(context: Context): List<PackageInfo> {
val sharedLibs = context.packageManager.systemSharedLibraryNames ?: emptyArray()
return context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
@@ -57,6 +64,19 @@ object PackageUtil {
}
}
fun hasSupportedMicroG(context: Context): Boolean {
val isGoogle = CertUtil.isGoogleGMS(context, PACKAGE_NAME_MICRO_G)
// Do not check for MicroG if Google Play Services is installed
if (isGoogle) return false
return if (isHuawei) {
isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G_HUAWEI)
} else {
isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G)
}
}
fun isInstalled(context: Context, packageName: String): Boolean {
return try {
getPackageInfo(context, packageName, PackageManager.GET_META_DATA)
@@ -218,6 +238,20 @@ object PackageUtil {
}
}
fun getIconDrawableForPackage(context: Context, packageName: String): Drawable? {
val placeholder = AppCompatResources.getDrawable(context, R.drawable.bg_placeholder)
return try {
val packageInfo = context.packageManager.getPackageInfo(packageName, 0)
val applicationInfo = packageInfo.applicationInfo ?: return placeholder
applicationInfo.loadIcon(context.packageManager)
} catch (exception: Exception) {
Log.e(TAG, "Failed to get icon for package!", exception)
placeholder
}
}
private fun getAllSharedLibraries(context: Context, flags: Int = 0): List<SharedLibraryInfo> {
return if (isTAndAbove) {
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
import android.content.Context
import android.content.pm.PackageInfo
import android.util.AttributeSet
import androidx.core.content.pm.PackageInfoCompat
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
@@ -31,30 +29,29 @@ import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.store.R
import com.aurora.store.data.model.MinimalApp
import com.aurora.store.databinding.ViewPackageBinding
import com.aurora.store.util.PackageUtil
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class PackageInfoView @JvmOverloads constructor(
class InstalledAppView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewPackageBinding>(context, attrs, defStyleAttr) {
@ModelProp(options = [ModelProp.Option.IgnoreRequireHashCode])
fun packageInfo(packageInfo: PackageInfo) {
val appInfo = packageInfo.applicationInfo!!
binding.imgIcon.load(PackageUtil.getIconForPackage(context, appInfo.packageName)) {
fun packageInfo(app: MinimalApp) {
binding.imgIcon.load(app.icon) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(25F))
}
binding.txtLine1.text = appInfo.loadLabel(context.packageManager)
binding.txtLine2.text = appInfo.packageName
binding.txtLine3.text = ("${packageInfo.versionName}.${PackageInfoCompat.getLongVersionCode(packageInfo)}")
binding.txtLine1.text = app.displayName
binding.txtLine2.text = app.packageName
binding.txtLine3.text = ("${app.versionName} (${app.versionCode})")
}
@CallbackProp

View File

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

View File

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

View File

@@ -19,36 +19,47 @@
package com.aurora.store.view.ui.all
import android.content.pm.PackageInfo
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.MinimalApp
import com.aurora.store.databinding.FragmentGenericWithSearchBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.PackageInfoViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.all.InstalledViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.util.Calendar
@AndroidEntryPoint
class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
private val viewModel: InstalledViewModel by viewModels()
private val startForDocumentExport =
registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) {
if (it != null) exportInstalledApps(it) else toast(R.string.toast_fav_export_failed)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.packages.collect {
viewModel.apps.collect {
updateController(it)
}
}
@@ -67,38 +78,47 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
}
// Toolbar
binding.layoutToolbarNative.apply {
imgActionPrimary.visibility = View.VISIBLE
imgActionSecondary.visibility = View.GONE
imgActionPrimary.setOnClickListener { findNavController().navigateUp() }
inputSearch.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s.isNullOrEmpty()) {
updateController(viewModel.packages.value)
} else {
val filteredPackages = viewModel.packages.value?.filter {
it.applicationInfo!!.loadLabel(requireContext().packageManager)
.contains(s, true) || it.packageName.contains(s, true)
}
updateController(filteredPackages)
binding.toolbar.apply {
inflateMenu(R.menu.menu_import_export)
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_export -> {
startForDocumentExport.launch(
"aurora_store_apps_${Calendar.getInstance().time.time}.json"
)
true
}
}
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
else -> false
}
})
}
}
binding.searchBar.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s.isNullOrEmpty()) {
updateController(viewModel.apps.value)
} else {
val filteredPackages = viewModel.apps.value?.filter {
it.displayName.contains(s, true) || it.packageName.contains(s, true)
}
updateController(filteredPackages)
}
}
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
})
}
private fun updateController(packages: List<PackageInfo>?) {
private fun updateController(packages: List<App>?) {
binding.recycler.withModels {
setFilterDuplicates(true)
if (packages == null) {
@@ -116,12 +136,21 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
)
packages.forEach { app ->
add(
PackageInfoViewModel_()
AppListViewModel_()
.id(app.packageName.hashCode())
.packageInfo(app)
.click { _ -> openDetailsFragment(app.packageName) }
.app(app)
.click { _ ->
openDetailsFragment(
app.packageName,
app
)
}
.longClick { _ ->
openAppMenuSheet(MinimalApp.fromPackageInfo(requireContext(), app))
openAppMenuSheet(
MinimalApp.fromApp(
app
)
)
false
}
)
@@ -130,4 +159,8 @@ class AppsGamesFragment : BaseFragment<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
import android.content.pm.PackageInfo
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.databinding.FragmentGenericWithSearchBinding
import com.aurora.store.view.epoxy.views.BlackListViewModel_
@@ -35,12 +40,22 @@ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.all.BlacklistViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.util.Calendar
@AndroidEntryPoint
class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
private val viewModel: BlacklistViewModel by viewModels()
private val startForDocumentImport =
registerForActivityResult(ActivityResultContracts.OpenDocument()) {
if (it != null) importBlacklist(it) else toast(R.string.toast_black_import_failed)
}
private val startForDocumentExport =
registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) {
if (it != null) exportBlacklist(it) else toast(R.string.toast_black_export_failed)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -51,37 +66,62 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
}
// Toolbar
binding.layoutToolbarNative.apply {
imgActionPrimary.visibility = View.VISIBLE
imgActionSecondary.visibility = View.GONE
imgActionPrimary.setOnClickListener {
binding.toolbar.apply {
inflateMenu(R.menu.menu_blacklist)
setNavigationOnClickListener {
viewModel.blacklistProvider.blacklist = viewModel.selected
findNavController().navigateUp()
}
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_import -> {
startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE))
}
inputSearch.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s.isNullOrEmpty()) {
updateController(viewModel.packages.value)
} else {
val filteredPackages = viewModel.packages.value?.filter {
it.applicationInfo!!.loadLabel(requireContext().packageManager)
.contains(s, true) || it.packageName.contains(s, true)
}
updateController(filteredPackages)
R.id.action_export -> {
startForDocumentExport.launch(
"aurora_store_apps_${Calendar.getInstance().time.time}.json"
)
}
R.id.action_select_all -> {
viewModel.selectAll()
binding.recycler.requestModelBuild()
true
}
R.id.action_remove_all -> {
viewModel.removeAll()
binding.recycler.requestModelBuild()
true
}
}
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {}
})
true
}
}
binding.searchBar.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s.isNullOrEmpty()) {
updateController(viewModel.packages.value)
} else {
val filteredPackages = viewModel.packages.value?.filter {
it.applicationInfo!!.loadLabel(requireContext().packageManager)
.contains(s, true) || it.packageName.contains(s, true)
}
updateController(filteredPackages)
}
}
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
})
}
override fun onPause() {
@@ -125,4 +165,15 @@ class BlacklistFragment : BaseFragment<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.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@@ -30,7 +29,6 @@ import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.gplayapi.utils.CategoryUtil
import com.aurora.store.R
import com.aurora.store.data.model.ViewState
import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
@@ -57,9 +55,8 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
val genericCarouselController = CategoryCarouselController(this)
// Toolbar
binding.layoutToolbarNative.toolbar.apply {
binding.toolbar.apply {
title = args.title
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
setNavigationOnClickListener { findNavController().navigateUp() }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,6 @@ package com.aurora.store.view.ui.downloads
import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.Constants.GITLAB_URL
@@ -54,11 +53,7 @@ class DownloadFragment : BaseFragment<FragmentDownloadBinding>() {
super.onViewCreated(view, savedInstanceState)
// Toolbar
binding.layoutToolbarAction.toolbar.apply {
elevation = 0f
title = getString(R.string.title_download_manager)
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
inflateMenu(R.menu.menu_download_main)
binding.toolbar.apply {
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener {
when (it.itemId) {

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

View File

@@ -19,19 +19,35 @@
package com.aurora.store.view.ui.splash
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Intent
import android.net.UrlQuerySanitizer
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.extensions.hide
import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.runOnUiThread
import com.aurora.extensions.show
import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.store.R
import com.aurora.store.data.model.AuthState
import com.aurora.store.databinding.FragmentSplashBinding
import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_INTRO
@@ -44,8 +60,20 @@ import kotlinx.coroutines.launch
@AndroidEntryPoint
class SplashFragment : BaseFragment<FragmentSplashBinding>() {
private val TAG = SplashFragment::class.java.simpleName
private val viewModel: AuthViewModel by activityViewModels()
private val startForAccount =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val accountName = it.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
if (!accountName.isNullOrBlank()) {
requestAuthTokenForGoogle(accountName)
} else {
runOnUiThread { resetActions() }
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -57,9 +85,7 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
}
// Toolbar
binding.layoutToolbarAction.toolbar.apply {
elevation = 0f
inflateMenu(R.menu.menu_splash)
binding.toolbar.apply {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_blacklist_manager -> {
@@ -158,10 +184,10 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
private fun updateActionLayout(isVisible: Boolean) {
if (isVisible) {
binding.layoutAction.show()
binding.layoutToolbarAction.toolbar.visibility = View.VISIBLE
binding.toolbar.visibility = View.VISIBLE
} else {
binding.layoutAction.hide()
binding.layoutToolbarAction.toolbar.visibility = View.GONE
binding.toolbar.visibility = View.GONE
}
}
@@ -176,7 +202,29 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
binding.btnGoogle.addOnClickListener {
if (viewModel.authState.value != AuthState.Fetching) {
binding.btnGoogle.updateProgress(true)
findNavController().navigate(R.id.googleFragment)
if (isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext())) {
val accounts = fetchGoogleAccounts()
// Do not show selection dialog if there is only one account available
if (accounts.isNotEmpty() && accounts.size == 1) {
requestAuthTokenForGoogle(accounts.first().name)
return@addOnClickListener
}
Log.i(TAG, "Found supported microG, trying to request credentials")
val accountIntent = AccountManager.newChooseAccountIntent(
null,
null,
arrayOf(GOOGLE_ACCOUNT_TYPE),
null,
null,
null,
null
)
startForAccount.launch(accountIntent)
} else {
findNavController().navigate(R.id.googleFragment)
}
}
}
}
@@ -194,7 +242,8 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
}
private fun navigateToDefaultTab() {
val defaultDestination = Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB)
val defaultDestination =
Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB)
val directions =
when (requireArguments().getInt("destinationId", defaultDestination)) {
R.id.updatesFragment -> {
@@ -206,19 +255,49 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
2 -> SplashFragmentDirections.actionSplashFragmentToUpdatesFragment()
else -> SplashFragmentDirections.actionSplashFragmentToNavigationApps()
}
activity?.viewModelStore?.clear() // Clear ViewModelStore to avoid bugs with logout
requireActivity().viewModelStore.clear() // Clear ViewModelStore to avoid bugs with logout
findNavController().navigate(directions)
}
private fun getPackageName(): String {
// Navigation component cannot handle market scheme as its missing a valid host
return if (activity?.intent != null && activity?.intent?.scheme == "market") {
return if (requireActivity().intent != null && requireActivity().intent.scheme == "market") {
requireActivity().intent.data!!.getQueryParameter("id") ?: ""
} else if (activity?.intent != null && activity?.intent?.action == Intent.ACTION_SEND) {
val clipData = activity?.intent?.getStringExtra(Intent.EXTRA_TEXT) ?: ""
} else if (requireActivity().intent != null && requireActivity().intent.action == Intent.ACTION_SEND) {
val clipData = requireActivity().intent.getStringExtra(Intent.EXTRA_TEXT) ?: ""
UrlQuerySanitizer(clipData).getValue("id") ?: ""
} else {
requireArguments().getString("packageName") ?: ""
}
}
private fun fetchGoogleAccounts(): Array<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.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
@@ -34,7 +33,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import com.aurora.extensions.toast
import com.aurora.store.R
import com.aurora.store.data.providers.NativeDeviceInfoProvider
import com.aurora.store.databinding.FragmentGenericWithPagerBinding
import com.aurora.store.databinding.FragmentSpoofBinding
import com.aurora.store.util.PathUtil
import com.aurora.store.view.ui.commons.BaseFragment
import com.google.android.material.tabs.TabLayout
@@ -42,7 +41,7 @@ import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SpoofFragment : BaseFragment<FragmentGenericWithPagerBinding>() {
class SpoofFragment : BaseFragment<FragmentSpoofBinding>() {
private val TAG = SpoofFragment::class.java.simpleName
// Android is weird, even if export device config with proper mime type, it will refuse to open
@@ -63,11 +62,7 @@ class SpoofFragment : BaseFragment<FragmentGenericWithPagerBinding>() {
super.onViewCreated(view, savedInstanceState)
// Toolbar
binding.layoutActionToolbar.toolbar.apply {
elevation = 0f
title = getString(R.string.title_spoof_manager)
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
inflateMenu(R.menu.menu_import_export)
binding.toolbar.apply {
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener {
when (it.itemId) {

View File

@@ -21,11 +21,14 @@ package com.aurora.store.viewmodel.all
import android.content.Context
import android.content.pm.PackageInfo
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.PackageUtil
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -37,6 +40,7 @@ import javax.inject.Inject
@HiltViewModel
class BlacklistViewModel @Inject constructor(
val blacklistProvider: BlacklistProvider,
val gson: Gson,
@ApplicationContext private val context: Context
) : ViewModel() {
@@ -60,4 +64,44 @@ class BlacklistViewModel @Inject constructor(
}
}
}
fun selectAll() {
selected.addAll(packages.value?.map { it.packageName } ?: emptyList())
}
fun removeAll() {
selected.clear()
}
fun importBlacklist(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
try {
context.contentResolver.openInputStream(uri)?.use {
val importedSet: MutableSet<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.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.room.favourite.Favourite
@@ -39,26 +40,38 @@ class FavouriteViewModel @Inject constructor(
private val favouriteDao: FavouriteDao,
private val gson: Gson
) : ViewModel() {
private val TAG = FavouriteViewModel::class.java.simpleName
val favouritesList = favouriteDao.favourites()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun importFavourites(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
context.contentResolver.openInputStream(uri)?.use {
val importExport =
gson.fromJson(it.bufferedReader().readText(), ImportExport::class.java)
favouriteDao.insertAll(
importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) }
)
try {
context.contentResolver.openInputStream(uri)?.use {
val importExport = gson.fromJson(
it.bufferedReader().readText(),
ImportExport::class.java
)
favouriteDao.insertAll(
importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) }
)
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to import favourites", exception)
}
}
}
fun exportFavourites(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray())
try {
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray())
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to export favourites", exception)
}
}
}

View File

@@ -20,11 +20,17 @@
package com.aurora.store.viewmodel.all
import android.content.Context
import android.content.pm.PackageInfo
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.ImportExport
import com.aurora.store.util.PackageUtil
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -35,13 +41,16 @@ import javax.inject.Inject
@HiltViewModel
class InstalledViewModel @Inject constructor(
@ApplicationContext private val context: Context
@ApplicationContext private val context: Context,
private val blacklistProvider: BlacklistProvider,
private val gson: Gson,
private val webAppDetailsHelper: WebAppDetailsHelper
) : ViewModel() {
private val TAG = InstalledViewModel::class.java.simpleName
private val _packages = MutableStateFlow<List<PackageInfo>?>(null)
val packages = _packages.asStateFlow()
private val _apps = MutableStateFlow<List<App>?>(null)
val apps = _apps.asStateFlow()
init {
fetchApps()
@@ -50,10 +59,35 @@ class InstalledViewModel @Inject constructor(
fun fetchApps() {
viewModelScope.launch(Dispatchers.IO) {
try {
_packages.value = PackageUtil.getAllValidPackages(context)
val packages = PackageUtil.getAllValidPackages(context)
.filterNot { blacklistProvider.isBlacklisted(it.packageName) }
// Divide the list of packages into chunks of 100 & fetch app details
// 50 is a safe number to avoid hitting the rate limit or package size limit
val chunkedPackages = packages.chunked(50)
val allApps = chunkedPackages.flatMap { chunk ->
webAppDetailsHelper.getAppDetails(chunk.map { it.packageName })
}
_apps.emit(allApps)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps", exception)
}
}
}
fun exportApps(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
try {
val favourites: List<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
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.details.TestingProgramStatus
@@ -11,19 +13,26 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.AuroraApp
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.Report
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.FavouriteDao
import com.aurora.store.util.PackageUtil
import com.google.gson.GsonBuilder
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.lang.reflect.Modifier
@@ -32,6 +41,7 @@ import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@HiltViewModel
class AppDetailsViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val appDetailsHelper: AppDetailsHelper,
private val reviewsHelper: ReviewsHelper,
private val webDataSafetyHelper: WebDataSafetyHelper,
@@ -69,7 +79,10 @@ class AppDetailsViewModel @Inject constructor(
private val _favourite = MutableStateFlow<Boolean>(false)
val favourite = _favourite.asStateFlow()
val downloadsList get() = downloadHelper.downloadsList
val download = combine(app, downloadHelper.downloadsList) { a, list ->
if (a.packageName.isBlank()) return@combine null
list.find { d -> d.packageName == a.packageName }
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
fun fetchAppDetails(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
@@ -77,7 +90,9 @@ class AppDetailsViewModel @Inject constructor(
checkFavourite(packageName)
val app: App = appStash.getOrPut(packageName) {
appDetailsHelper.getAppByPackageName(packageName)
appDetailsHelper.getAppByPackageName(packageName).apply {
isInstalled = PackageUtil.isInstalled(context, packageName)
}
}
_app.emit(app)
@@ -148,20 +163,42 @@ class AppDetailsViewModel @Inject constructor(
}
}
fun fetchUserAppReview(app: App) {
viewModelScope.launch(Dispatchers.IO) {
try {
val stashedUserReview = userReviewStash[app.packageName]
if (stashedUserReview != null) {
_userReview.emit(stashedUserReview)
return@launch
}
val isTesting = app.testingProgram?.isSubscribed ?: false
val userReview = reviewsHelper.getUserReview(app.packageName, isTesting)
if (userReview != null) {
userReviewStash[app.packageName] = userReview
_userReview.emit(userReview)
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch user review", exception)
}
}
}
fun postAppReview(packageName: String, review: Review, isBeta: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
val userReview = userReviewStash.getOrPut(packageName) {
reviewsHelper.addOrEditReview(
packageName,
review.title,
review.comment,
review.rating,
isBeta
)
}
val userReview = reviewsHelper.addOrEditReview(
packageName,
review.title,
review.comment,
review.rating,
isBeta
)
if (userReview != null) {
context.toast(R.string.toast_rated_success)
userReviewStash[packageName] = userReview
_userReview.emit(userReview)
}
} catch (exception: Exception) {
@@ -206,8 +243,8 @@ class AppDetailsViewModel @Inject constructor(
private fun getLatestExodusReport(packageName: String): Report? {
val headers: MutableMap<String, String> = mutableMapOf()
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
headers["Content-Type"] = Constants.JSON_MIME_TYPE
headers["Accept"] = Constants.JSON_MIME_TYPE
headers["Authorization"] = "Token ${BuildConfig.EXODUS_API_KEY}"
val url = Constants.EXODUS_SEARCH_URL + packageName

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"
tools:context=".view.ui.about.AboutFragment">
<include
android:id="@+id/layout_toolbar_action"
layout="@layout/view_toolbar_action" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:layout_width="match_parent"

View File

@@ -27,9 +27,14 @@
android:orientation="vertical"
tools:context=".view.ui.account.AccountFragment">
<include
android:id="@+id/layout_toolbar_action"
layout="@layout/view_toolbar_action" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/chip_layout"
@@ -140,4 +145,4 @@
app:btnStateText="@string/action_logout" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

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

View File

@@ -26,9 +26,14 @@
android:weightSum="3"
tools:context=".view.ui.about.AboutFragment">
<include
android:id="@+id/layout_toolbar_action"
layout="@layout/view_toolbar_action" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:layout_width="match_parent"

View File

@@ -28,9 +28,14 @@
android:weightSum="2"
tools:context=".view.ui.account.AccountFragment">
<include
android:id="@+id/layout_toolbar_action"
layout="@layout/view_toolbar_action" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:layout_width="match_parent"
@@ -131,4 +136,4 @@
app:btnStateIcon="@drawable/ic_logout"
app:btnStateText="@string/action_logout" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -31,9 +31,14 @@
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/layout_details_toolbar"
layout="@layout/view_toolbar_native" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/scrollView"
@@ -119,8 +124,4 @@
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
<include
android:id="@+id/layout_details_install"
layout="@layout/layout_details_install" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -25,9 +25,13 @@
android:orientation="vertical"
tools:context=".view.ui.details.DetailsMoreFragment">
<include
android:id="@+id/layout_toolbar_action_more"
layout="@layout/view_toolbar_action" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:layout_width="match_parent"
@@ -100,4 +104,4 @@
tools:listitem="@layout/view_file" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</LinearLayout>

View File

@@ -25,9 +25,13 @@
android:orientation="vertical"
tools:context=".view.ui.details.DetailsReviewFragment">
<include
android:id="@+id/layout_toolbar_action_review"
layout="@layout/view_toolbar_action" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/sort_view"
@@ -53,6 +57,13 @@
android:layout_height="wrap_content"
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
android:id="@+id/filter_review_critical"
style="@style/Widget.Material3.Chip.Filter"
@@ -117,4 +128,4 @@
app:itemSpacing="@dimen/margin_normal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_review" />
</LinearLayout>
</LinearLayout>

View File

@@ -27,10 +27,13 @@
android:showDividers="middle"
tools:context=".view.ui.details.DevProfileFragment">
<include
android:id="@+id/layout_toolbar_action"
layout="@layout/view_toolbar_action" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/view_flipper"
@@ -98,4 +101,4 @@
android:layout_centerInParent="true" />
</RelativeLayout>
</ViewFlipper>
</LinearLayout>
</LinearLayout>

View File

@@ -25,9 +25,15 @@
android:orientation="vertical"
tools:context=".view.ui.downloads.DownloadFragment">
<include
android:id="@+id/layout_toolbar_action"
layout="@layout/view_toolbar_native" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/recycler"
@@ -38,4 +44,4 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:stackFromEnd="false"
tools:listitem="@layout/view_download" />
</LinearLayout>
</LinearLayout>

View File

@@ -24,18 +24,35 @@
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/layout_toolbar_native"
layout="@layout/view_toolbar_search" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/layout_toolbar_native"
android:layout_below="@+id/toolbar"
android:clipToPadding="true"
android:paddingBottom="@dimen/height_bottom_adj"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:stackFromEnd="false"
tools:listitem="@layout/view_app_list" />
</RelativeLayout>
</RelativeLayout>

View File

@@ -23,17 +23,21 @@
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/layout_toolbar_native"
layout="@layout/view_toolbar_native" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/layout_toolbar_native"
android:layout_below="@+id/toolbar"
android:clipToPadding="true"
android:paddingBottom="@dimen/height_bottom_adj"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:stackFromEnd="false" />
</RelativeLayout>
</RelativeLayout>

View File

@@ -30,15 +30,33 @@
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/layout_view_toolbar"
layout="@layout/view_toolbar_search" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/layout_view_toolbar" />
android:layout_below="@id/toolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler"

View File

@@ -19,6 +19,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -29,15 +30,33 @@
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/layout_toolbar_search"
layout="@layout/view_toolbar_search" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/layout_toolbar_search" />
android:layout_below="@id/toolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler"
@@ -46,4 +65,4 @@
android:layout_below="@+id/divider"
tools:listitem="@layout/view_search_suggestion" />
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

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

View File

@@ -22,15 +22,21 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/layout_action_toolbar"
layout="@layout/view_toolbar_native" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
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
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/layout_action_toolbar"
android:layout_below="@id/toolbar"
android:background="@android:color/transparent"
app:tabGravity="fill"
app:tabIndicator="@drawable/tab_indicator"
@@ -44,4 +50,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/tab_layout" />
</RelativeLayout>
</RelativeLayout>

View File

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

View File

@@ -18,6 +18,7 @@
-->
<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="wrap_content"
@@ -26,18 +27,35 @@
android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_icon"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
tools:src="@drawable/bg_placeholder" />
<RelativeLayout
android:id="@+id/img_icon_layout"
android:layout_width="@dimen/icon_size_large"
android:layout_height="@dimen/icon_size_large">
<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
android:id="@+id/txt_line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_toEndOf="@id/img_icon"
android:layout_toEndOf="@id/img_icon_layout"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Aurora.SubTitle"
tools:text="App Name" />
@@ -84,4 +102,31 @@
android:layout_alignEnd="@id/txt_line1"
android:textAppearance="@style/TextAppearance.Aurora.Line2"
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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical"
android:paddingStart="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small"
android:showDividers="middle"
android:visibility="gone"
tools:visibility="visible">
@@ -40,7 +42,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/margin_small"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
@@ -61,11 +62,9 @@
<com.google.android.material.button.MaterialButton
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:insetTop="@dimen/margin_xsmall"
android:insetBottom="@dimen/margin_xsmall"
android:minWidth="128dp"
android:text="@string/action_join"
app:cornerRadius="@dimen/radius_small" />
</LinearLayout>

View File

@@ -34,70 +34,52 @@
android:orientation="vertical">
<HorizontalScrollView
android:id="@+id/layout_extras"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
<com.google.android.material.chip.ChipGroup
android:id="@+id/layout_extras"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:divider="@drawable/divider_line"
android:dividerPadding="@dimen/padding_xsmall"
android:orientation="horizontal"
android:padding="@dimen/padding_small"
android:showDividers="middle">
android:paddingStart="@dimen/padding_medium"
android:paddingEnd="@dimen/padding_medium"
app:selectionRequired="false"
app:singleLine="true">
<androidx.appcompat.widget.AppCompatTextView
<com.google.android.material.chip.Chip
android:id="@+id/txt_rating"
style="@style/Chip.Tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
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"
app:chipIcon="@drawable/ic_star"
tools:text="3.5" />
<androidx.appcompat.widget.AppCompatTextView
<com.google.android.material.chip.Chip
android:id="@+id/txt_installs"
style="@style/Chip.Tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
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"
app:chipIcon="@drawable/ic_download_manager"
tools:text="2500" />
<androidx.appcompat.widget.AppCompatTextView
<com.google.android.material.chip.Chip
android:id="@+id/txt_size"
style="@style/Chip.Tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableTint="?colorAccent"
app:chipIcon="@drawable/ic_apk_install"
tools:text="25 MB" />
<androidx.appcompat.widget.AppCompatTextView
<com.google.android.material.chip.Chip
android:id="@+id/txt_updated"
style="@style/Chip.Tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:drawablePadding="@dimen/padding_small"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
app:drawableTint="?colorAccent"
app:chipIcon="@drawable/ic_updates"
tools:text="Jan 21 2020" />
</LinearLayout>
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<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"
android:id="@+id/exodus_card"
android:layout_width="match_parent"
@@ -41,20 +41,14 @@
android:id="@+id/txt_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/header_privacy"
android:text="@string/exodus_progress"
android:textAppearance="@style/TextAppearance.Aurora.Line1" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.TonalButton"
android:id="@+id/btn_request_analysis"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_width="match_parent"
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"
app:cornerRadius="@dimen/margin_small" />
</RelativeLayout>
</LinearLayout>

View File

@@ -38,8 +38,9 @@
android:id="@+id/layout_user_review"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<RatingBar
android:id="@+id/user_stars"
@@ -55,38 +56,39 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/margin_normal"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="middle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_title"
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="42dp"
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" />
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_review"
<com.google.android.material.textfield.TextInputEditText
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_height="42dp"
android:background="@drawable/bg_changelog"
android:gravity="center_vertical|center_horizontal"
android:hint="@string/details_think_this_app"
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" />
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_review"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:hint="@string/details_think_this_app"
android:inputType="textMultiLine"
android:textAppearance="@style/TextAppearance.Aurora.Line2" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_post_review"
@@ -94,7 +96,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/action_post"
app:cornerRadius="@dimen/margin_small" />
app:cornerRadius="@dimen/radius_small" />
</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
android:id="@+id/filter_gfs"
style="@style/AppTheme.FilterChip"
style="@style/Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_filter_gsf_dependent_apps" />
<com.google.android.material.chip.Chip
android:id="@+id/filter_paid"
style="@style/AppTheme.FilterChip"
style="@style/Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_filter_paid_apps" />
<com.google.android.material.chip.Chip
android:id="@+id/filter_ads"
style="@style/AppTheme.FilterChip"
style="@style/Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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">
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/radius_large" />
<solid android:color="?android:colorBackground" />
</shape>
</item>
</layer-list>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_select_all"
android:title="@string/action_select_all" />
<item
android:id="@+id/action_remove_all"
android:title="@string/action_remove_all" />
<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:name="com.aurora.store.view.ui.all.AppsGamesFragment"
android:label="@string/title_apps_games"
tools:layout="@layout/fragment_generic_with_pager" />
tools:layout="@layout/fragment_generic_with_search" />
<fragment
android:id="@+id/spoofFragment"
android:name="com.aurora.store.view.ui.spoof.SpoofFragment"
android:label="@string/title_spoof_manager"
tools:layout="@layout/fragment_generic_with_pager" />
tools:layout="@layout/fragment_spoof" />
<fragment
android:id="@+id/favouriteFragment"
android:name="com.aurora.store.view.ui.commons.FavouriteFragment"

View File

@@ -2,7 +2,7 @@
<resources>
<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="android:statusBarColor">@color/colorTransparent</item>
</style>

View File

@@ -28,12 +28,6 @@
<attr name="btnStateIcon" format="reference" />
</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">
<attr name="minWidth" format="dimension" />
<attr name="maxWidth" format="dimension" />
@@ -48,4 +42,4 @@
<attr name="txtSubtitle" format="string" />
<attr name="imgIcon" format="reference" />
</declare-styleable>
</resources>
</resources>

View File

@@ -85,10 +85,12 @@
<string name="action_pause">"Pause"</string>
<string name="action_pending">"Pending"</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_restart">"Restart"</string>
<string name="action_resume">"Resume"</string>
<string name="action_search">"Search"</string>
<string name="action_select_all">"Select all"</string>
<string name="action_share">"Share"</string>
<string name="action_uninstall">"Uninstall"</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_view_report">"View report"</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_five">"Five"</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_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 -->
<string name="export_app_title">File Exporter</string>
<string name="export_app_summary">Hold on, exporting your file</string>

View File

@@ -20,7 +20,7 @@
<resources>
<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="bottomSheetStyle">@style/AppTheme.BottomSheetStyle</item>
<item name="android:statusBarColor">@color/colorTransparent</item>
@@ -47,5 +47,17 @@
<item name="widgetLayout">@layout/preference_material_switch</item>
</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>

View File

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