Initial support for third-party dispensers

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-05-21 21:06:49 +05:30
parent 9004aa6df8
commit 453f6f24c2
21 changed files with 434 additions and 31 deletions

View File

@@ -4,9 +4,11 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.aurora.Constants
import com.aurora.extensions.isSAndAbove
import com.aurora.store.data.work.CacheWorker
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.Preferences.PREFERENCE_INTRO
import com.aurora.store.util.Preferences.PREFERENCE_MIGRATION_VERSION
import com.aurora.store.util.Preferences.PREFERENCE_THEME_ACCENT
@@ -43,6 +45,7 @@ class MigrationReceiver: BroadcastReceiver() {
if (currentVersion == 0) {
CacheWorker.scheduleAutomatedCacheCleanup(context)
if (isSAndAbove()) context.save(PREFERENCE_THEME_ACCENT, 0)
context.save(PREFERENCE_DISPENSER_URLS, setOf(Constants.URL_DISPENSER))
currentVersion++
}

View File

@@ -39,6 +39,11 @@ object CertUtil {
return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName)
}
fun isAppGalleryApp(context: Context, packageName: String): Boolean {
val appGalleryPackageName = "com.huawei.appmarket"
return context.packageManager.getInstallerPackageNameCompat(packageName) == appGalleryPackageName
}
fun getEncodedCertificateHashes(context: Context, packageName: String): List<String> {
return try {
val certificates = getX509Certificates(context, packageName)

View File

@@ -53,6 +53,7 @@ object Preferences {
const val PREFERENCE_PROXY_INFO = "PREFERENCE_PROXY_INFO"
const val PREFERENCE_PROXY_ENABLED = "PREFERENCE_PROXY_ENABLED"
const val PREFERENCE_DISPENSER_URLS = "PREFERENCE_DISPENSER_URLS"
const val PREFERENCE_VENDING_VERSION = "PREFERENCE_VENDING_VERSION"
const val PREFERENCE_UPDATES_EXTENDED = "PREFERENCE_UPDATES_EXTENDED"
@@ -74,6 +75,10 @@ object Preferences {
getPrefs(context).edit().putString(key, value).apply()
}
fun putStringSet(context: Context, key: String, value: Set<String>) {
getPrefs(context).edit().putStringSet(key, value).apply()
}
fun putInteger(context: Context, key: String, value: Int) {
getPrefs(context).edit().putInt(key, value).apply()
}
@@ -94,6 +99,10 @@ object Preferences {
return getPrefs(context).getString(key, default).toString()
}
fun getStringSet(context: Context, key: String, default: Set<String> = emptySet()): Set<String> {
return getPrefs(context).getStringSet(key, default) ?: emptySet()
}
fun getInteger(context: Context, key: String, default: Int = 0): Int {
return getPrefs(context).getInt(key, default)
}
@@ -119,9 +128,13 @@ fun Context.save(key: String, value: Boolean) = Preferences.putBoolean(this, key
fun Context.save(key: String, value: String) = Preferences.putString(this, key, value)
fun Context.save(key: String, value: Set<String>) = Preferences.putStringSet(this, key, value)
fun Fragment.save(key: String, value: Int) = requireContext().save(key, value)
fun Fragment.save(key: String, value: Boolean) = requireContext().save(key, value)
fun Fragment.save(key: String, value: String) = requireContext().save(key, value)
fun Fragment.save(key: String, value: Set<String>) = requireContext().save(key, value)

View File

@@ -0,0 +1,55 @@
package com.aurora.store.view.epoxy.views
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.store.R
import com.aurora.store.databinding.ViewDispenserBinding
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseView::class
)
class DispenserView : RelativeLayout {
private lateinit var binding: ViewDispenserBinding
constructor(context: Context?) : super(context) {
init(context)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init(context)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context)
}
private fun init(context: Context?) {
val view = inflate(context, R.layout.view_dispenser, this)
binding = ViewDispenserBinding.bind(view)
}
@ModelProp
fun url(url: String) {
binding.url.text = url
}
@CallbackProp
fun copy(onClickListener: OnClickListener?) {
binding.url.setOnClickListener(onClickListener)
}
@CallbackProp
fun clear(onClickListener: OnClickListener?) {
binding.btnAction.setOnClickListener(onClickListener)
}
}

View File

@@ -0,0 +1,71 @@
package com.aurora.store.view.ui.dispenser
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.aurora.extensions.copyToClipBoard
import com.aurora.store.R
import com.aurora.store.databinding.FragmentDispenserBinding
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.view.epoxy.views.DispenserViewModel_
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class DispenserFragment : Fragment(R.layout.fragment_dispenser),
SharedPreferences.OnSharedPreferenceChangeListener {
private var _binding: FragmentDispenserBinding? = null
private val binding get() = _binding!!
private val dispensers: Set<String>
get() = Preferences.getStringSet(requireContext(), PREFERENCE_DISPENSER_URLS)
private lateinit var sharedPreferences: SharedPreferences
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentDispenserBinding.bind(view)
sharedPreferences = Preferences.getPrefs(view.context)
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
binding.addFab.setOnClickListener {
findNavController().navigate(R.id.inputDispenserDialog)
}
setupRecyclerView()
}
override fun onDestroyView() {
super.onDestroyView()
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
_binding = null
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == PREFERENCE_DISPENSER_URLS) setupRecyclerView()
}
private fun setupRecyclerView() {
binding.epoxyRecycler.withModels {
setFilterDuplicates(true)
dispensers.forEachIndexed { index, url ->
add(
DispenserViewModel_()
.id(index)
.url(url)
.copy { _ -> requireContext().copyToClipBoard(url) }
.clear { _ ->
findNavController().navigate(
DispenserFragmentDirections
.actionDispenserFragmentToRemoveDispenserDialog(url)
)
}
)
}
}
}
}

View File

@@ -0,0 +1,53 @@
package com.aurora.store.view.ui.dispenser
import android.app.Dialog
import android.os.Bundle
import android.util.Patterns
import android.view.WindowManager
import androidx.fragment.app.DialogFragment
import com.aurora.extensions.showKeyboard
import com.aurora.extensions.toast
import com.aurora.store.R
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.save
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
class InputDispenserDialog: DialogFragment() {
private val textInputLayout: TextInputLayout?
get() = dialog?.findViewById(R.id.textInputLayout)
private val dispensers: Set<String>
get() = Preferences.getStringSet(requireContext(), PREFERENCE_DISPENSER_URLS)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val view = layoutInflater.inflate(R.layout.dialog_dispenser_input, null)
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.add_dispenser_title)
.setMessage(R.string.add_dispenser_summary)
.setView(view)
.setPositiveButton(getString(R.string.add)) { _, _ -> saveDispenserUrl() }
.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dialog?.dismiss()}
.create()
}
override fun onResume() {
super.onResume()
textInputLayout?.editText?.showKeyboard()
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
private fun saveDispenserUrl() {
val url = textInputLayout?.editText?.text?.toString()
if (!url.isNullOrEmpty() && Patterns.WEB_URL.matcher(url).matches()) {
val newSet = dispensers.toMutableSet().apply {
add(url)
}
save(PREFERENCE_DISPENSER_URLS, newSet)
} else {
toast(R.string.add_dispenser_error)
}
}
}

View File

@@ -0,0 +1,35 @@
package com.aurora.store.view.ui.dispenser
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.navArgs
import com.aurora.store.R
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.save
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class RemoveDispenserDialog : DialogFragment() {
private val args: RemoveDispenserDialogArgs by navArgs()
private val dispensers: Set<String>
get() = Preferences.getStringSet(requireContext(), PREFERENCE_DISPENSER_URLS)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.remove_dispenser_title)
.setMessage(getString(R.string.remove_dispenser_summary, args.url))
.setPositiveButton(getString(R.string.remove)) { _, _ -> removeDispenserUrl() }
.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dialog?.dismiss() }
.create()
}
private fun removeDispenserUrl() {
val newSet = dispensers.toMutableSet().apply {
remove(args.url)
}
save(PREFERENCE_DISPENSER_URLS, newSet)
}
}

View File

@@ -27,17 +27,20 @@ import androidx.lifecycle.Lifecycle
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.aurora.Constants
import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.extensions.isSAndAbove
import com.aurora.store.R
import com.aurora.store.data.work.CacheWorker
import com.aurora.store.data.work.UpdateWorker
import com.aurora.store.databinding.FragmentOnboardingBinding
import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_GOOGLE
@@ -162,6 +165,10 @@ class OnboardingFragment : Fragment(R.layout.fragment_onboarding) {
save(PREFERENCE_FILTER_SEARCH, true)
/*Network*/
// TODO: Gather feedback and drop setting default dispenser for all builds
if (!CertUtil.isAppGalleryApp(requireContext(), requireContext().packageName)) {
save(PREFERENCE_DISPENSER_URLS, setOf(Constants.URL_DISPENSER))
}
save(PREFERENCE_INSECURE_ANONYMOUS, false)
save(PREFERENCE_VENDING_VERSION, 0)

View File

@@ -46,6 +46,13 @@ class NetworkPreference : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_network, rootKey)
findPreference<Preference>(Preferences.PREFERENCE_DISPENSER_URLS)?.apply {
setOnPreferenceClickListener {
findNavController().navigate(R.id.dispenserFragment)
true
}
}
findPreference<EditTextPreference>(Preferences.PREFERENCE_PROXY_URL)?.let {
it.summary =
Preferences.getString(requireContext(), Preferences.PREFERENCE_PROXY_URL, "")

View File

@@ -86,6 +86,12 @@ class SplashFragment : Fragment(R.layout.fragment_splash) {
attachActions()
// Show anonymous logins if we have dispenser URL
if (!viewModel.dispenserURL.isNullOrBlank()) {
binding.btnAnonymous.visibility = View.VISIBLE
binding.btnAnonymousInsecure.visibility = View.VISIBLE
}
//Initial status
updateStatus(getString(R.string.session_init))

View File

@@ -43,6 +43,7 @@ import com.aurora.store.data.providers.SpoofProvider
import com.aurora.store.util.AC2DMTask
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_AUTH_DATA
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -66,6 +67,12 @@ class AuthViewModel @Inject constructor(
private val spoofProvider = SpoofProvider(context)
val dispenserURL: String?
get() {
val dispensers = Preferences.getStringSet(context, PREFERENCE_DISPENSER_URLS)
return if (dispensers.isNotEmpty()) dispensers.random() else null
}
val liveData: MutableLiveData<AuthState> = MutableLiveData()
fun observe() {
@@ -119,7 +126,7 @@ class AuthViewModel @Inject constructor(
val playResponse = HttpClient
.getPreferredClient(context)
.postAuth(
Constants.URL_DISPENSER,
dispenserURL!!,
gson.toJson(properties).toByteArray()
)
@@ -154,7 +161,7 @@ class AuthViewModel @Inject constructor(
val playResponse = HttpClient
.getPreferredClient(context)
.getAuth(Constants.URL_DISPENSER)
.getAuth(dispenserURL!!)
if (playResponse.isSuccessful) {
val insecureAuth = gson.fromJson(