Redo Permissions [1/2]

This commit is contained in:
Rahul Patel
2024-07-27 13:56:40 +05:30
parent 894e7a629f
commit 44b9a1a7b2
15 changed files with 407 additions and 475 deletions

View File

@@ -19,17 +19,23 @@
package com.aurora.extensions package com.aurora.extensions
import android.Manifest
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.verify.domain.DomainVerificationManager
import android.content.pm.verify.domain.DomainVerificationUserState
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.os.PowerManager import android.os.PowerManager
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.aurora.Constants import com.aurora.Constants
@@ -131,7 +137,9 @@ fun Context.accentColor(): Int {
fun Context.isIgnoringBatteryOptimizations(): Boolean { fun Context.isIgnoringBatteryOptimizations(): Boolean {
return if (isMAndAbove()) { return if (isMAndAbove()) {
(getSystemService(Context.POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(packageName) (getSystemService(Context.POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(
packageName
)
} else { } else {
true true
} }
@@ -148,3 +156,26 @@ fun Context.restartApp() {
exitProcess(0) exitProcess(0)
} }
fun Context.checkManifestPermission(permission: String): Boolean {
return ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
fun Context.isExternalStorageAccessible(): Boolean {
return if (isRAndAbove()) {
Environment.isExternalStorageManager()
} else {
checkManifestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
fun Context.isDomainVerified(domain: String): Boolean {
return if (isSAndAbove()) {
val domainVerificationManager = getSystemService(DomainVerificationManager::class.java)
val userState = domainVerificationManager.getDomainVerificationUserState(packageName)
val domainMap = userState?.hostToStateMap?.filterKeys { it == domain }
domainMap?.values?.first() == DomainVerificationUserState.DOMAIN_STATE_SELECTED
} else {
true
}
}

View File

@@ -0,0 +1,10 @@
package com.aurora.store
enum class PermissionType {
EXTERNAL_STORAGE,
STORAGE_MANAGER,
INSTALL_UNKNOWN_APPS,
POST_NOTIFICATIONS,
DOZE_WHITELIST,
APP_LINKS
}

View File

@@ -19,19 +19,22 @@
package com.aurora.store.data.model package com.aurora.store.data.model
import com.aurora.store.PermissionType
data class Permission( data class Permission(
var id: Int, val type: PermissionType,
var title: String, val title: String,
var subtitle: String val subtitle: String,
val optional: Boolean = false,
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return when (other) { return when (other) {
is Permission -> other.id == id is Permission -> other.type == type
else -> false else -> false
} }
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return id.hashCode() return type.hashCode()
} }
} }

View File

@@ -0,0 +1,122 @@
package com.aurora.store.data.providers
import android.Manifest
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.PowerManager
import android.provider.Settings
import android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.aurora.extensions.checkManifestPermission
import com.aurora.extensions.isDomainVerified
import com.aurora.extensions.isExternalStorageAccessible
import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.isOAndAbove
import com.aurora.store.BuildConfig
import com.aurora.store.PermissionType
import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil
@SuppressLint("NewApi")
class PermissionProvider : ActivityResultCallback<ActivityResult> {
private var context: Context
private var intentLauncher: ActivityResultLauncher<Intent>
private var permissionLauncher: ActivityResultLauncher<String>
constructor(activity: AppCompatActivity) {
context = activity
intentLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
this
)
permissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
}
constructor(fragment: Fragment) {
context = fragment.requireContext()
intentLauncher = fragment.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
this
)
permissionLauncher =
fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
}
fun unregister() {
intentLauncher.unregister()
permissionLauncher.unregister()
}
private val permissionMap: Map<PermissionType, Intent> = mapOf(
PermissionType.STORAGE_MANAGER to PackageUtil.getStorageManagerIntent(safe = true),
PermissionType.INSTALL_UNKNOWN_APPS to Intent(
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
Uri.parse("package:${BuildConfig.APPLICATION_ID}")
),
PermissionType.DOZE_WHITELIST to Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:${BuildConfig.APPLICATION_ID}")
),
PermissionType.APP_LINKS to Intent(
ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:${BuildConfig.APPLICATION_ID}")
)
)
fun requestPermission(permissionType: PermissionType) {
try {
when (permissionType) {
PermissionType.EXTERNAL_STORAGE -> permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
PermissionType.POST_NOTIFICATIONS -> permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
else -> {
val intent = permissionMap[permissionType] ?: return
intentLauncher.launch(intent)
}
}
} catch (e: ActivityNotFoundException) {
Log.e("PermissionProvider", "Activity not found for $permissionType")
}
}
fun isPermissionGranted(permissionType: PermissionType): Boolean {
return when (permissionType) {
PermissionType.EXTERNAL_STORAGE,
PermissionType.STORAGE_MANAGER -> context.isExternalStorageAccessible()
PermissionType.POST_NOTIFICATIONS -> context.checkManifestPermission(Manifest.permission.POST_NOTIFICATIONS)
PermissionType.INSTALL_UNKNOWN_APPS -> {
if (isOAndAbove())
context.packageManager.canRequestPackageInstalls()
else
true
}
PermissionType.DOZE_WHITELIST -> {
if (isMAndAbove()) {
val powerManager =
context.getSystemService(Context.POWER_SERVICE) as PowerManager
powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)
} else {
true
}
}
PermissionType.APP_LINKS -> context.isDomainVerified("play.google.com") || context.isDomainVerified(
"market.android.com"
)
}
}
override fun onActivityResult(result: ActivityResult) {
// We don't need to do anything here, as we anyway do permission re-checks using @isPermissionGranted
}
}

View File

@@ -1,13 +0,0 @@
package com.aurora.store.util
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
fun isExternalStorageAccessible(context: Context): Boolean {
return ActivityCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
}

View File

@@ -0,0 +1,42 @@
/*
* 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.epoxy.views
import android.content.Context
import android.util.AttributeSet
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.store.databinding.ViewTextDividerBinding
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class TextDividerView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewTextDividerBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun title(title: String) {
binding.txtTitle.text = title
}
}

View File

@@ -20,19 +20,15 @@
package com.aurora.store.view.ui.details package com.aurora.store.view.ui.details
import android.Manifest
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@@ -46,7 +42,6 @@ import com.aurora.Constants.EXODUS_SUBMIT_PAGE
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.getString import com.aurora.extensions.getString
import com.aurora.extensions.hide import com.aurora.extensions.hide
import com.aurora.extensions.isRAndAbove
import com.aurora.extensions.runOnUiThread import com.aurora.extensions.runOnUiThread
import com.aurora.extensions.share import com.aurora.extensions.share
import com.aurora.extensions.show import com.aurora.extensions.show
@@ -57,6 +52,7 @@ import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.AppStreamStash import com.aurora.store.AppStreamStash
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.PermissionType
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event import com.aurora.store.data.event.Event
@@ -67,6 +63,7 @@ import com.aurora.store.data.model.State
import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState
import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.providers.PermissionProvider
import com.aurora.store.databinding.FragmentDetailsBinding import com.aurora.store.databinding.FragmentDetailsBinding
import com.aurora.store.util.CertUtil import com.aurora.store.util.CertUtil
import com.aurora.store.util.CommonUtil import com.aurora.store.util.CommonUtil
@@ -103,26 +100,9 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
@Inject @Inject
lateinit var authProvider: AuthProvider lateinit var authProvider: AuthProvider
private lateinit var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>
private val startForStorageManagerResult = private lateinit var permissionProvider: PermissionProvider
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private lateinit var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>
if (isRAndAbove() && Environment.isExternalStorageManager()) {
viewModel.download(app)
} else {
flip(0)
toast(R.string.permissions_denied)
}
}
private val startForPermissions =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
viewModel.download(app)
} else {
flip(0)
toast(R.string.permissions_denied)
}
}
private lateinit var app: App private lateinit var app: App
@@ -192,6 +172,11 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
permissionProvider = PermissionProvider(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -378,6 +363,11 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
super.onResume() super.onResume()
} }
override fun onDestroy() {
permissionProvider.unregister()
super.onDestroy()
}
private fun attachActions() { private fun attachActions() {
flip(0) flip(0)
checkAndSetupInstall() checkAndSetupInstall()
@@ -560,26 +550,10 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
updateActionState(State.PROGRESS) updateActionState(State.PROGRESS)
if (PathUtil.needsStorageManagerPerm(app.fileList)) { if (PathUtil.needsStorageManagerPerm(app.fileList)) {
if (isRAndAbove()) { if (permissionProvider.isPermissionGranted(PermissionType.STORAGE_MANAGER)) {
if (!Environment.isExternalStorageManager()) { viewModel.download(app)
try {
startForStorageManagerResult.launch(PackageUtil.getStorageManagerIntent())
} catch (_: ActivityNotFoundException) {
startForStorageManagerResult.launch(PackageUtil.getStorageManagerIntent(true))
}
} else {
viewModel.download(app)
}
} else { } else {
if (ContextCompat.checkSelfPermission( permissionProvider.requestPermission(PermissionType.STORAGE_MANAGER)
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
viewModel.download(app)
} else {
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
} }
} else { } else {
viewModel.download(app) viewModel.download(app)

View File

@@ -1,108 +1,15 @@
package com.aurora.store.view.ui.onboarding package com.aurora.store.view.ui.onboarding
import android.content.Intent
import android.content.pm.verify.domain.DomainVerificationManager
import android.content.pm.verify.domain.DomainVerificationUserState
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS
import android.util.Log
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import com.aurora.extensions.isSAndAbove
import com.aurora.extensions.toast
import com.aurora.store.R
import com.aurora.store.databinding.FragmentAppLinksBinding import com.aurora.store.databinding.FragmentAppLinksBinding
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.google.android.material.button.MaterialButton
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class AppLinksFragment : BaseFragment<FragmentAppLinksBinding>() { class AppLinksFragment : BaseFragment<FragmentAppLinksBinding>() {
private val TAG = AppLinksFragment::class.java.simpleName
private val playStoreDomain = "play.google.com"
private val marketDomain = "market.android.com"
// AppLink buttons
private lateinit var buttons: Map<String, MaterialButton>
private val startForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (isSAndAbove() && buttons.keys.any { domainVerified(it) }) {
toast(R.string.app_link_enabled)
}
updateButtonState()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
buttons = mapOf(
playStoreDomain to binding.playStoreButton,
marketDomain to binding.marketButton,
)
updateButtonState()
if (isSAndAbove()) {
buttons.values.forEach {
it.setOnClickListener {
try {
val intent = Intent(
ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:${view.context.packageName}")
)
startForResult.launch(intent)
} catch (exception: Exception) {
Log.e(TAG, "Failed to open app links screen", exception)
toast(R.string.failed_app_link)
}
}
}
}
} }
override fun onDestroyView() {
buttons = emptyMap()
super.onDestroyView()
}
private fun updateButtonState() {
if (isSAndAbove()) {
buttons.forEach { (domain, button) ->
button.apply {
text = if (domainVerified(domain)) {
getString(R.string.action_enabled)
} else {
getString(R.string.action_enable)
}
isEnabled = !domainVerified(domain)
}
}
} else {
buttons.forEach { (_, button) ->
button.apply {
text = getString(R.string.action_enabled)
isEnabled = false
}
}
}
}
private fun domainVerified(domain: String): Boolean {
return if (isSAndAbove()) {
val domainVerificationManager = requireContext().getSystemService(
DomainVerificationManager::class.java
)
val userState = domainVerificationManager.getDomainVerificationUserState(
requireContext().packageName
)
val domainMap = userState?.hostToStateMap?.filterKeys { it == domain }
domainMap?.values?.first() == DomainVerificationUserState.DOMAIN_STATE_SELECTED
} else {
true
}
}
} }

View File

@@ -76,7 +76,7 @@ class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return 3 return 2
} }
} }
@@ -122,9 +122,9 @@ class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
fun refreshButtonState() { fun refreshButtonState() {
binding.btnBackward.isEnabled = lastPosition != 0 binding.btnBackward.isEnabled = lastPosition != 0
binding.btnForward.isEnabled = lastPosition != 2 binding.btnForward.isEnabled = lastPosition != 1
if (lastPosition == 2) { if (lastPosition == 1) {
binding.btnForward.text = getString(R.string.action_finish) binding.btnForward.text = getString(R.string.action_finish)
binding.btnForward.isEnabled = true binding.btnForward.isEnabled = true
binding.btnForward.setOnClickListener { finishOnboarding() } binding.btnForward.setOnClickListener { finishOnboarding() }
@@ -177,7 +177,10 @@ class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
} }
private fun setupAutoUpdates() { private fun setupAutoUpdates() {
save(PREFERENCE_UPDATES_AUTO, if (requireContext().isIgnoringBatteryOptimizations()) 2 else 1) save(
PREFERENCE_UPDATES_AUTO,
if (requireContext().isIgnoringBatteryOptimizations()) 2 else 1
)
UpdateWorker.scheduleAutomatedCheck(requireContext()) UpdateWorker.scheduleAutomatedCheck(requireContext())
} }
} }

View File

@@ -20,223 +20,143 @@
package com.aurora.store.view.ui.onboarding package com.aurora.store.view.ui.onboarding
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.os.PowerManager
import android.provider.Settings
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import com.aurora.extensions.isMAndAbove import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isRAndAbove import com.aurora.extensions.isRAndAbove
import com.aurora.extensions.isSAndAbove
import com.aurora.extensions.isTAndAbove import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.toast import com.aurora.store.PermissionType
import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.Permission import com.aurora.store.data.model.Permission
import com.aurora.store.data.providers.PermissionProvider
import com.aurora.store.databinding.FragmentOnboardingPermissionsBinding import com.aurora.store.databinding.FragmentOnboardingPermissionsBinding
import com.aurora.store.util.PackageUtil import com.aurora.store.view.epoxy.views.TextDividerViewModel_
import com.aurora.store.util.isExternalStorageAccessible
import com.aurora.store.view.epoxy.views.preference.PermissionViewModel_ import com.aurora.store.view.epoxy.views.preference.PermissionViewModel_
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class PermissionsFragment : BaseFragment<FragmentOnboardingPermissionsBinding>() { class PermissionsFragment : BaseFragment<FragmentOnboardingPermissionsBinding>() {
private lateinit var permissionProvider: PermissionProvider
private val startForDozeResult = override fun onCreate(savedInstanceState: Bundle?) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { super.onCreate(savedInstanceState)
context?.let { permissionProvider = PermissionProvider(this)
val powerManager = it.getSystemService(Context.POWER_SERVICE) as PowerManager }
if (isMAndAbove() && powerManager.isIgnoringBatteryOptimizations(it.packageName)) {
toast(R.string.toast_permission_granted)
binding.epoxyRecycler.requestModelBuild()
}
}
}
private val startForPackageManagerResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (isOAndAbove() && requireContext().packageManager.canRequestPackageInstalls()) {
toast(R.string.toast_permission_granted)
binding.epoxyRecycler.requestModelBuild()
}
}
private val startForStorageManagerResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (isRAndAbove() && Environment.isExternalStorageManager()) {
toast(R.string.toast_permission_granted)
binding.epoxyRecycler.requestModelBuild()
}
}
private val startForPermissions =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
toast(R.string.toast_permission_granted)
binding.epoxyRecycler.requestModelBuild()
} else {
toast(R.string.permissions_denied)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
updateController()
}
// RecyclerView override fun onResume() {
val installerList = mutableListOf( super.onResume()
updateController()
}
override fun onDestroy() {
permissionProvider.unregister()
super.onDestroy()
}
private fun permissionList(): List<Permission> {
val permissions = mutableListOf(
Permission( Permission(
2, PermissionType.INSTALL_UNKNOWN_APPS,
getString(R.string.onboarding_permission_installer), getString(R.string.onboarding_permission_installer),
getString(R.string.onboarding_permission_installer_desc) getString(R.string.onboarding_permission_installer_desc)
) )
) )
if (isMAndAbove()) {
installerList.add(
Permission(
4,
getString(R.string.onboarding_permission_doze),
getString(R.string.onboarding_permission_doze_desc)
)
)
}
if (isRAndAbove()) { if (isRAndAbove()) {
installerList.add( permissions.add(
Permission( Permission(
1, PermissionType.STORAGE_MANAGER,
getString(R.string.onboarding_permission_esm), getString(R.string.onboarding_permission_esm),
getString(R.string.onboarding_permission_esa_desc) getString(R.string.onboarding_permission_esa_desc),
false
) )
) )
} else { } else {
installerList.add( permissions.add(
Permission( Permission(
0, PermissionType.EXTERNAL_STORAGE,
getString(R.string.onboarding_permission_esa), getString(R.string.onboarding_permission_esa),
getString(R.string.onboarding_permission_esa_desc) getString(R.string.onboarding_permission_esa_desc),
false
) )
) )
} }
if (isTAndAbove()) {
installerList.add(
Permission(
3,
getString(R.string.onboarding_permission_notifications),
getString(R.string.onboarding_permission_notifications_desc)
)
)
}
binding.epoxyRecycler.withModels {
val dozeDisabled = if (isMAndAbove()) {
val powerManager = context?.getSystemService(Context.POWER_SERVICE) as PowerManager
powerManager.isIgnoringBatteryOptimizations(requireContext().packageName)
} else {
true
}
val writeExternalStorage =
if (!isRAndAbove()) isExternalStorageAccessible(requireContext()) else true
val postNotifications = if (isTAndAbove()) ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED else true
val storageManager = if (isRAndAbove()) Environment.isExternalStorageManager() else true
setFilterDuplicates(true)
installerList.forEach {
add(
PermissionViewModel_()
.id(it.id)
.permission(it)
.isGranted(
when (it.id) {
0 -> writeExternalStorage
1 -> storageManager
2 -> canInstallPackages()
3 -> postNotifications
4 -> dozeDisabled
else -> false
}
)
.click { _ ->
when (it.id) {
0 -> checkStorageAccessPermission()
1 -> requestStorageManagerPermission()
2 -> requestPackageManagerPermission()
3 -> checkPostNotificationsPermission()
4 -> requestDozePermission()
}
}
)
}
}
}
private fun canInstallPackages(): Boolean {
if (isOAndAbove()) {
return requireContext().packageManager.canRequestPackageInstalls()
} else {
return true
}
}
private fun requestDozePermission() {
if (isMAndAbove()) { if (isMAndAbove()) {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { permissions.add(
data = Uri.parse("package:${requireContext().packageName}") Permission(
} PermissionType.DOZE_WHITELIST,
startForDozeResult.launch(intent) getString(R.string.onboarding_permission_doze),
} getString(R.string.onboarding_permission_doze_desc),
} true
private fun checkStorageAccessPermission() {
if (canInstallPackages()) {
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
toast(R.string.toast_permission_installer_required)
}
}
private fun checkPostNotificationsPermission() {
if (isTAndAbove()) {
startForPermissions.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
private fun requestStorageManagerPermission() {
if (isRAndAbove()) {
if (canInstallPackages()) {
try {
startForStorageManagerResult.launch(PackageUtil.getStorageManagerIntent())
} catch (_: ActivityNotFoundException) {
startForStorageManagerResult.launch(PackageUtil.getStorageManagerIntent(true))
}
} else {
toast(R.string.toast_permission_installer_required)
}
}
}
private fun requestPackageManagerPermission() {
if (isOAndAbove()) {
startForPackageManagerResult.launch(
Intent(
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
Uri.parse("package:${BuildConfig.APPLICATION_ID}")
) )
) )
} }
if (isTAndAbove()) {
permissions.add(
Permission(
PermissionType.POST_NOTIFICATIONS,
getString(R.string.onboarding_permission_notifications),
getString(R.string.onboarding_permission_notifications_desc),
true
)
)
}
if (isSAndAbove()) {
permissions.add(
Permission(
PermissionType.APP_LINKS,
"App Links",
"Enable Aurora Store to open links from the Play Store",
optional = true
),
)
}
return permissions
} }
private fun updateController() {
binding.epoxyRecycler.withModels {
setFilterDuplicates(true)
add(
TextDividerViewModel_()
.id("required_divider")
.title(getString(R.string.item_required))
)
permissionList()
.filterNot { it.optional }
.forEach { add(renderPermissionView(it)) }
val optionalPermissions = permissionList().filter { it.optional }
if (optionalPermissions.isNotEmpty()) {
add(
TextDividerViewModel_()
.id("optional_divider")
.title(getString(R.string.item_optional))
)
optionalPermissions.forEach { add(renderPermissionView(it)) }
}
}
}
private fun renderPermissionView(permission: Permission): PermissionViewModel_ {
return PermissionViewModel_()
.id(permission.type.name)
.permission(permission)
.isGranted(permissionProvider.isPermissionGranted(permission.type))
.click { _ -> permissionProvider.requestPermission(permission.type) }
}
} }

View File

@@ -19,28 +19,21 @@
package com.aurora.store.view.ui.updates package com.aurora.store.view.ui.updates
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.aurora.Constants import com.aurora.Constants
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.isRAndAbove
import com.aurora.extensions.toast
import com.aurora.store.MobileNavigationDirections import com.aurora.store.MobileNavigationDirections
import com.aurora.store.PermissionType
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.MinimalApp import com.aurora.store.data.model.MinimalApp
import com.aurora.store.data.providers.PermissionProvider
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.update.Update import com.aurora.store.data.room.update.Update
import com.aurora.store.databinding.FragmentUpdatesBinding import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PathUtil import com.aurora.store.util.PathUtil
import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_ import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppUpdateViewModel_ import com.aurora.store.view.epoxy.views.app.AppUpdateViewModel_
@@ -56,23 +49,15 @@ import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() { class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
private lateinit var permissionProvider: PermissionProvider
private lateinit var update: Update private lateinit var update: Update
private val viewModel: UpdatesViewModel by viewModels() private val viewModel: UpdatesViewModel by viewModels()
private val startForStorageManagerResult = override fun onCreate(savedInstanceState: Bundle?) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { super.onCreate(savedInstanceState)
if (isRAndAbove() && Environment.isExternalStorageManager()) { permissionProvider = PermissionProvider(this)
viewModel.download(update) }
} else {
toast(R.string.permissions_denied)
}
}
private val startForPermissions =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { perm ->
if (perm) viewModel.download(update) else toast(R.string.permissions_denied)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -124,6 +109,11 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
} }
} }
override fun onDestroy() {
permissionProvider.unregister()
super.onDestroy()
}
private fun updateController(appList: Map<Update, Download?>?) { private fun updateController(appList: Map<Update, Download?>?) {
binding.recycler.withModels { binding.recycler.withModels {
setFilterDuplicates(true) setFilterDuplicates(true)
@@ -204,26 +194,10 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
viewModel.updateAllEnqueued = updateAll viewModel.updateAllEnqueued = updateAll
if (PathUtil.needsStorageManagerPerm(update.fileList)) { if (PathUtil.needsStorageManagerPerm(update.fileList)) {
if (isRAndAbove()) { if (permissionProvider.isPermissionGranted(PermissionType.STORAGE_MANAGER)) {
if (!Environment.isExternalStorageManager()) { viewModel.download(update)
try {
startForStorageManagerResult.launch(PackageUtil.getStorageManagerIntent())
} catch (_: ActivityNotFoundException) {
startForStorageManagerResult.launch(PackageUtil.getStorageManagerIntent(true))
}
} else {
viewModel.download(update)
}
} else { } else {
if (ContextCompat.checkSelfPermission( permissionProvider.requestPermission(PermissionType.STORAGE_MANAGER)
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
viewModel.download(update)
} else {
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
} }
} else { } else {
viewModel.download(update) viewModel.download(update)

View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".view.ui.onboarding.AppLinksFragment"> tools:context=".view.ui.onboarding.AppLinksFragment">
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
@@ -35,91 +36,12 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appCompatTextView2" /> app:layout_constraintTop_toBottomOf="@+id/appCompatTextView2" />
<ScrollView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxyRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent" android:clipToPadding="false"
app:layout_constraintEnd_toEndOf="parent" android:overScrollMode="never"
app:layout_constraintStart_toStartOf="parent" android:padding="@dimen/padding_medium"
app:layout_constraintTop_toBottomOf="@+id/appCompatTextView"> tools:listitem="@layout/view_permission_switch" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/padding_medium">
<LinearLayout
android:id="@+id/playStoreLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/padding_small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appCompatTextView">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/playStoreTitle"
style="@style/AuroraTextStyle.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_link_play_store" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/playStoreDesc"
style="@style/AuroraTextStyle.Line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="5"
android:text="@string/google_play_store_desc" />
<com.google.android.material.button.MaterialButton
android:id="@+id/playStoreButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:insetTop="@dimen/margin_xsmall"
android:insetBottom="@dimen/margin_xsmall"
android:text="@string/action_enable"
android:textColor="?colorSurface"
app:cornerRadius="@dimen/margin_small" />
</LinearLayout>
<LinearLayout
android:id="@+id/marketLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/padding_small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playStoreLayout">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/marketTitle"
style="@style/AuroraTextStyle.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_link_android_market" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/marketDesc"
style="@style/AuroraTextStyle.Line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="5"
android:text="@string/android_market_desc" />
<com.google.android.material.button.MaterialButton
android:id="@+id/marketButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:insetTop="@dimen/margin_xsmall"
android:insetBottom="@dimen/margin_xsmall"
android:text="@string/action_enable"
android:textColor="?colorSurface"
app:cornerRadius="@dimen/margin_small" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,12 +17,11 @@
~ ~
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/padding_small"> android:padding="@dimen/padding_small">
@@ -31,24 +30,25 @@
style="@style/AuroraTextStyle.Subtitle" style="@style/AuroraTextStyle.Subtitle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Title" /> android:layout_toStartOf="@id/btn_action"
tools:text="Permission Title" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/line2" android:id="@+id/line2"
style="@style/AuroraTextStyle.Line2" style="@style/AuroraTextStyle.Line2"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="5" android:layout_below="@id/line1"
tools:text="Description" /> android:layout_toStartOf="@id/btn_action"
android:maxLines="3"
tools:text="Permission Description" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_action" android:id="@+id/btn_action"
style="@style/Widget.Material3.Button" style="@style/Widget.Material3.Button.TextButton.Dialog.Flush"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:insetTop="@dimen/margin_xsmall" android:layout_alignParentEnd="true"
android:insetBottom="@dimen/margin_xsmall"
android:text="@string/action_grant" android:text="@string/action_grant"
android:textColor="?colorSurface" app:iconPadding="@dimen/padding_small" />
app:cornerRadius="@dimen/radius_small" /> </RelativeLayout>
</LinearLayout>

View File

@@ -0,0 +1,35 @@
<?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:tools="http://schemas.android.com/tools"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_small">
<TextView
android:id="@+id/txt_title"
style="@style/AuroraTextStyle.Line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="true"
android:textColor="?colorPrimary"
tools:text="Text" />
</RelativeLayout>

View File

@@ -348,6 +348,8 @@
<string name="menu">Menu</string> <string name="menu">Menu</string>
<string name="expand">Expand</string> <string name="expand">Expand</string>
<string name="spoof_property"><xliff:g id="manufacturer">%1$s</xliff:g> • API <xliff:g id="api">%2$s</xliff:g></string> <string name="spoof_property"><xliff:g id="manufacturer">%1$s</xliff:g> • API <xliff:g id="api">%2$s</xliff:g></string>
<string name="item_required">Required</string>
<string name="item_optional">Optional</string>
<string name="permissions_denied">Required permissions were denied. Please grant them in order to continue the action</string> <string name="permissions_denied">Required permissions were denied. Please grant them in order to continue the action</string>
<string name="search_hint">Search for Apps &amp; Games</string> <string name="search_hint">Search for Apps &amp; Games</string>
<string name="requesting_new_session">Requesting new session</string> <string name="requesting_new_session">Requesting new session</string>