diff --git a/app/src/main/java/com/aurora/store/data/receiver/MigrationReceiver.kt b/app/src/main/java/com/aurora/store/data/receiver/MigrationReceiver.kt index 85def4b82..d68927f5a 100644 --- a/app/src/main/java/com/aurora/store/data/receiver/MigrationReceiver.kt +++ b/app/src/main/java/com/aurora/store/data/receiver/MigrationReceiver.kt @@ -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++ } diff --git a/app/src/main/java/com/aurora/store/util/CertUtil.kt b/app/src/main/java/com/aurora/store/util/CertUtil.kt index 66fc400ea..2a6a977f0 100644 --- a/app/src/main/java/com/aurora/store/util/CertUtil.kt +++ b/app/src/main/java/com/aurora/store/util/CertUtil.kt @@ -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 { return try { val certificates = getX509Certificates(context, packageName) diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt index 543adec92..4dd91a8d9 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -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) { + 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 = emptySet()): Set { + 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) = 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) = requireContext().save(key, value) diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/DispenserView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/DispenserView.kt new file mode 100644 index 000000000..dd6023503 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/DispenserView.kt @@ -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) + } +} diff --git a/app/src/main/java/com/aurora/store/view/ui/dispenser/DispenserFragment.kt b/app/src/main/java/com/aurora/store/view/ui/dispenser/DispenserFragment.kt new file mode 100644 index 000000000..776685437 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/ui/dispenser/DispenserFragment.kt @@ -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 + 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) + ) + } + ) + } + } + } +} diff --git a/app/src/main/java/com/aurora/store/view/ui/dispenser/InputDispenserDialog.kt b/app/src/main/java/com/aurora/store/view/ui/dispenser/InputDispenserDialog.kt new file mode 100644 index 000000000..3ed4f73c1 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/ui/dispenser/InputDispenserDialog.kt @@ -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 + 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) + } + } +} diff --git a/app/src/main/java/com/aurora/store/view/ui/dispenser/RemoveDispenserDialog.kt b/app/src/main/java/com/aurora/store/view/ui/dispenser/RemoveDispenserDialog.kt new file mode 100644 index 000000000..8aebb5bef --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/ui/dispenser/RemoveDispenserDialog.kt @@ -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 + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt b/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt index 6291b8cf1..f15a8d8a8 100644 --- a/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt @@ -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) diff --git a/app/src/main/java/com/aurora/store/view/ui/preferences/NetworkPreference.kt b/app/src/main/java/com/aurora/store/view/ui/preferences/NetworkPreference.kt index fa06c70d9..e1e777e94 100644 --- a/app/src/main/java/com/aurora/store/view/ui/preferences/NetworkPreference.kt +++ b/app/src/main/java/com/aurora/store/view/ui/preferences/NetworkPreference.kt @@ -46,6 +46,13 @@ class NetworkPreference : BasePreferenceFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.preferences_network, rootKey) + findPreference(Preferences.PREFERENCE_DISPENSER_URLS)?.apply { + setOnPreferenceClickListener { + findNavController().navigate(R.id.dispenserFragment) + true + } + } + findPreference(Preferences.PREFERENCE_PROXY_URL)?.let { it.summary = Preferences.getString(requireContext(), Preferences.PREFERENCE_PROXY_URL, "") diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt index 3e4416fc8..4e53672d4 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt @@ -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)) diff --git a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt index 4235bb72d..f441408ac 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt @@ -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 = 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( diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 000000000..d70710e92 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_server.xml b/app/src/main/res/drawable/ic_server.xml new file mode 100644 index 000000000..d4b74e939 --- /dev/null +++ b/app/src/main/res/drawable/ic_server.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout-land/fragment_splash.xml b/app/src/main/res/layout-land/fragment_splash.xml index 5676000a0..152fe0179 100644 --- a/app/src/main/res/layout-land/fragment_splash.xml +++ b/app/src/main/res/layout-land/fragment_splash.xml @@ -76,11 +76,12 @@ tools:text="Sign in first" /> - + android:text="@string/account_login_using" /> @@ -129,11 +123,9 @@ style="@style/AuroraTextStyle.Line3" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:layout_centerHorizontal="true" android:layout_marginTop="@dimen/margin_small" android:textAlignment="center" tools:text="Version 4.0.1" /> - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/dialog_dispenser_input.xml b/app/src/main/res/layout/dialog_dispenser_input.xml new file mode 100644 index 000000000..0018c7fbe --- /dev/null +++ b/app/src/main/res/layout/dialog_dispenser_input.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_dispenser.xml b/app/src/main/res/layout/fragment_dispenser.xml new file mode 100644 index 000000000..802d69e9b --- /dev/null +++ b/app/src/main/res/layout/fragment_dispenser.xml @@ -0,0 +1,47 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml index d63185f2f..f0755cc1b 100644 --- a/app/src/main/res/layout/fragment_splash.xml +++ b/app/src/main/res/layout/fragment_splash.xml @@ -70,12 +70,12 @@ tools:text="Sign in first" /> - + android:text="@string/account_login_using" /> @@ -104,8 +100,7 @@ android:id="@+id/btn_anonymous" android:layout_width="@dimen/width_button" android:layout_height="wrap_content" - android:layout_below="@id/btn_google" - android:layout_centerHorizontal="true" + android:visibility="gone" app:btnStateIcon="@drawable/ic_anonymous" app:btnStateText="@string/account_anonymous" /> @@ -113,8 +108,7 @@ android:id="@+id/btn_anonymous_insecure" android:layout_width="@dimen/width_button" android:layout_height="wrap_content" - android:layout_below="@id/btn_anonymous" - android:layout_centerHorizontal="true" + android:visibility="gone" app:btnStateIcon="@drawable/ic_anonymous" app:btnStateText="@string/account_anonymous_insecure" /> @@ -123,10 +117,8 @@ style="@style/AuroraTextStyle.Line3" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:layout_centerHorizontal="true" android:layout_marginTop="@dimen/margin_small" android:textAlignment="center" tools:text="Version 4.0.1" /> - + diff --git a/app/src/main/res/layout/view_dispenser.xml b/app/src/main/res/layout/view_dispenser.xml new file mode 100644 index 000000000..4402a5d3c --- /dev/null +++ b/app/src/main/res/layout/view_dispenser.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 91ecec87b..45924712e 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -394,6 +394,15 @@ android:name="com.aurora.store.view.ui.preferences.InstallerFragment" android:label="InstallerFragment" tools:layout="@layout/fragment_installer"/> + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81b70e191..f139a742a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -436,4 +436,18 @@ Clear device owner Removes Aurora Store as the device owner app This will revoke session installer\'s permission to silently install app. Proceed with clearing device ownership? + + + Manage dispensers + View and manage token dispensers for anonymous logins + + + Add dispenser + Add a token dispenser to Aurora Store. Token dispensers provide account credentials to Aurora Store for logging in anonymously. + Invalid URL + Dispenser URL + Remove dispenser + Do you wish to remove the dispenser \"%1$s\"? + Add + Remove diff --git a/app/src/main/res/xml/preferences_network.xml b/app/src/main/res/xml/preferences_network.xml index 7ff1c92dc..131ea389d 100644 --- a/app/src/main/res/xml/preferences_network.xml +++ b/app/src/main/res/xml/preferences_network.xml @@ -19,6 +19,13 @@ + +