Issue #7425 (et al): Cache the list of installed browsers
Cache the list of installed browsers. Calling `Browsers.all` the application directly redundantly recalculates the list. Accessing the list of installed browsers through this cache will reduce that overhead.master
parent
98cc869976
commit
9c28cb632c
|
@ -60,6 +60,7 @@ import org.mozilla.fenix.settings.SettingsFragmentDirections
|
||||||
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
|
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
|
||||||
import org.mozilla.fenix.theme.DefaultThemeManager
|
import org.mozilla.fenix.theme.DefaultThemeManager
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
|
import org.mozilla.fenix.utils.BrowsersCache
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
open class HomeActivity : LocaleAwareAppCompatActivity() {
|
open class HomeActivity : LocaleAwareAppCompatActivity() {
|
||||||
|
@ -139,6 +140,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
|
||||||
hotStartMonitor.onPostResumeFinalMethodCall()
|
hotStartMonitor.onPostResumeFinalMethodCall()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
// Every time the application goes into the background, it is possible that the user
|
||||||
|
// is about to change the browsers installed on their system. Therefore, we reset the cache of
|
||||||
|
// all the installed browsers.
|
||||||
|
//
|
||||||
|
// NB: There are ways for the user to install new products without leaving the browser.
|
||||||
|
BrowsersCache.resetAll()
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Handles intents received when the activity is open.
|
* Handles intents received when the activity is open.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -13,7 +13,6 @@ import mozilla.components.service.glean.Glean
|
||||||
import mozilla.components.service.glean.config.Configuration
|
import mozilla.components.service.glean.config.Configuration
|
||||||
import mozilla.components.service.glean.net.ConceptFetchHttpUploader
|
import mozilla.components.service.glean.net.ConceptFetchHttpUploader
|
||||||
import mozilla.components.service.glean.private.NoExtraKeys
|
import mozilla.components.service.glean.private.NoExtraKeys
|
||||||
import mozilla.components.support.utils.Browsers
|
|
||||||
import org.mozilla.fenix.GleanMetrics.BookmarksManagement
|
import org.mozilla.fenix.GleanMetrics.BookmarksManagement
|
||||||
import org.mozilla.fenix.GleanMetrics.Collections
|
import org.mozilla.fenix.GleanMetrics.Collections
|
||||||
import org.mozilla.fenix.GleanMetrics.ContextMenu
|
import org.mozilla.fenix.GleanMetrics.ContextMenu
|
||||||
|
@ -45,6 +44,7 @@ import org.mozilla.fenix.GleanMetrics.ToolbarSettings
|
||||||
import org.mozilla.fenix.GleanMetrics.TrackingProtection
|
import org.mozilla.fenix.GleanMetrics.TrackingProtection
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.utils.BrowsersCache
|
||||||
|
|
||||||
private class EventWrapper<T : Enum<T>>(
|
private class EventWrapper<T : Enum<T>>(
|
||||||
private val recorder: ((Map<T, String>?) -> Unit),
|
private val recorder: ((Map<T, String>?) -> Unit),
|
||||||
|
@ -504,7 +504,7 @@ class GleanMetricsService(private val context: Context) : MetricsService {
|
||||||
|
|
||||||
internal fun setStartupMetrics() {
|
internal fun setStartupMetrics() {
|
||||||
Metrics.apply {
|
Metrics.apply {
|
||||||
defaultBrowser.set(Browsers.all(context).isDefaultBrowser)
|
defaultBrowser.set(BrowsersCache.all(context).isDefaultBrowser)
|
||||||
MozillaProductDetector.getMozillaBrowserDefault(context)?.also {
|
MozillaProductDetector.getMozillaBrowserDefault(context)?.also {
|
||||||
defaultMozBrowser.set(it)
|
defaultMozBrowser.set(it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ package org.mozilla.fenix.components.metrics
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import mozilla.components.support.utils.Browsers
|
import org.mozilla.fenix.utils.BrowsersCache
|
||||||
|
|
||||||
object MozillaProductDetector {
|
object MozillaProductDetector {
|
||||||
enum class MozillaProducts(val productName: String) {
|
enum class MozillaProducts(val productName: String) {
|
||||||
|
@ -57,7 +57,7 @@ object MozillaProductDetector {
|
||||||
* Returns the default browser if and only if it is a Mozilla product.
|
* Returns the default browser if and only if it is a Mozilla product.
|
||||||
*/
|
*/
|
||||||
fun getMozillaBrowserDefault(context: Context): String? {
|
fun getMozillaBrowserDefault(context: Context): String? {
|
||||||
val browserPackageName = Browsers.all(context).defaultBrowser?.packageName
|
val browserPackageName = BrowsersCache.all(context).defaultBrowser?.packageName
|
||||||
return if (isMozillaProduct(browserPackageName)) { browserPackageName } else { null }
|
return if (isMozillaProduct(browserPackageName)) { browserPackageName } else { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import android.util.AttributeSet
|
||||||
import android.widget.Switch
|
import android.widget.Switch
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceViewHolder
|
import androidx.preference.PreferenceViewHolder
|
||||||
import mozilla.components.support.utils.Browsers
|
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.utils.BrowsersCache
|
||||||
|
|
||||||
class DefaultBrowserPreference @JvmOverloads constructor(
|
class DefaultBrowserPreference @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -31,7 +31,7 @@ class DefaultBrowserPreference @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSwitch() {
|
fun updateSwitch() {
|
||||||
val browsers = Browsers.all(context)
|
val browsers = BrowsersCache.all(context)
|
||||||
switchView?.isChecked = browsers.isDefaultBrowser
|
switchView?.isChecked = browsers.isDefaultBrowser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,13 @@ import android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import mozilla.components.support.utils.Browsers
|
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ext.getPreferenceKey
|
import org.mozilla.fenix.ext.getPreferenceKey
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.ext.showToolbar
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
|
import org.mozilla.fenix.utils.BrowsersCache
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lets the user control their default browser preferences
|
* Lets the user control their default browser preferences
|
||||||
|
@ -55,7 +55,7 @@ class DefaultBrowserSettingsFragment : PreferenceFragmentCompat() {
|
||||||
settings.unsetOpenLinksInAPrivateTabIfNecessary()
|
settings.unsetOpenLinksInAPrivateTabIfNecessary()
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>(getPreferenceKey(R.string.pref_key_open_links_in_a_private_tab))?.apply {
|
findPreference<CheckBoxPreference>(getPreferenceKey(R.string.pref_key_open_links_in_a_private_tab))?.apply {
|
||||||
isEnabled = Browsers.all(requireContext()).isDefaultBrowser
|
isEnabled = BrowsersCache.all(requireContext()).isDefaultBrowser
|
||||||
isChecked = settings.openLinksInAPrivateTab
|
isChecked = settings.openLinksInAPrivateTab
|
||||||
onPreferenceChangeListener = SharedPreferenceUpdater()
|
onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.support.utils.Browsers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches the list of browsers installed on a user's device.
|
||||||
|
*
|
||||||
|
* BrowsersCache caches the list of installed browsers is gathered lazily when it is first accessed
|
||||||
|
* after initial creation or invalidation. For that reason, a context is required every time
|
||||||
|
* the cache is accessed.
|
||||||
|
*
|
||||||
|
* Users are responsible for invalidating the cache at the appropriate time. It is left up to the
|
||||||
|
* user to determine appropriate policies for maintaining the validity of the cache. If, when the
|
||||||
|
* cache is accessed, it is filled, the contents will be returned. As mentioned above, the cache
|
||||||
|
* will be lazily refilled after invalidation. In other words, invalidation is O(1).
|
||||||
|
*
|
||||||
|
* This cache is threadsafe.
|
||||||
|
*/
|
||||||
|
object BrowsersCache {
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var cachedBrowsers: Browsers? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun all(context: Context): Browsers {
|
||||||
|
run {
|
||||||
|
val cachedBrowsers = cachedBrowsers
|
||||||
|
if (cachedBrowsers != null) {
|
||||||
|
return cachedBrowsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Browsers.all(context).also {
|
||||||
|
this.cachedBrowsers = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun resetAll() {
|
||||||
|
cachedBrowsers = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,6 @@ import mozilla.components.support.ktx.android.content.floatPreference
|
||||||
import mozilla.components.support.ktx.android.content.intPreference
|
import mozilla.components.support.ktx.android.content.intPreference
|
||||||
import mozilla.components.support.ktx.android.content.longPreference
|
import mozilla.components.support.ktx.android.content.longPreference
|
||||||
import mozilla.components.support.ktx.android.content.stringPreference
|
import mozilla.components.support.ktx.android.content.stringPreference
|
||||||
import mozilla.components.support.utils.Browsers
|
|
||||||
import org.mozilla.fenix.BuildConfig
|
import org.mozilla.fenix.BuildConfig
|
||||||
import org.mozilla.fenix.Config
|
import org.mozilla.fenix.Config
|
||||||
import org.mozilla.fenix.FeatureFlags
|
import org.mozilla.fenix.FeatureFlags
|
||||||
|
@ -375,7 +374,7 @@ class Settings private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unsetOpenLinksInAPrivateTabIfNecessary() {
|
fun unsetOpenLinksInAPrivateTabIfNecessary() {
|
||||||
if (Browsers.all(appContext).isDefaultBrowser) {
|
if (BrowsersCache.all(appContext).isDefaultBrowser) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package org.mozilla.fenix.utils
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import mozilla.components.support.utils.Browsers
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.TestApplication
|
||||||
|
import org.robolectric.Shadows.shadowOf
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@Config(application = TestApplication::class)
|
||||||
|
class BrowsersCacheTest {
|
||||||
|
|
||||||
|
// NB: There is always one more browser than pretendBrowsersAreInstalled installs because
|
||||||
|
// the application we are testing is recognized as a browser itself!
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cached list of browsers match before-after installation when cache is not invalidated`() {
|
||||||
|
BrowsersCache.resetAll()
|
||||||
|
pretendBrowsersAreInstalled(
|
||||||
|
browsers = listOf(
|
||||||
|
Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
|
||||||
|
Browsers.KnownBrowser.REFERENCE_BROWSER.packageName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val initialBrowserList = BrowsersCache.all(testContext)
|
||||||
|
assertEquals(3, initialBrowserList.installedBrowsers.size)
|
||||||
|
|
||||||
|
pretendBrowsersAreInstalled(
|
||||||
|
browsers = listOf(
|
||||||
|
Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
|
||||||
|
Browsers.KnownBrowser.FIREFOX.packageName,
|
||||||
|
Browsers.KnownBrowser.CHROME.packageName,
|
||||||
|
Browsers.KnownBrowser.SAMSUNG_INTERNET.packageName,
|
||||||
|
Browsers.KnownBrowser.DUCKDUCKGO.packageName,
|
||||||
|
Browsers.KnownBrowser.REFERENCE_BROWSER.packageName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val updatedBrowserList = BrowsersCache.all(testContext)
|
||||||
|
assertEquals(3, updatedBrowserList.installedBrowsers.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cached list of browsers change before-after installation when cache is invalidated`() {
|
||||||
|
BrowsersCache.resetAll()
|
||||||
|
pretendBrowsersAreInstalled(
|
||||||
|
browsers = listOf(
|
||||||
|
Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
|
||||||
|
Browsers.KnownBrowser.REFERENCE_BROWSER.packageName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val initialBrowserList = BrowsersCache.all(testContext)
|
||||||
|
assertEquals(3, initialBrowserList.installedBrowsers.size)
|
||||||
|
|
||||||
|
pretendBrowsersAreInstalled(
|
||||||
|
browsers = listOf(
|
||||||
|
Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
|
||||||
|
Browsers.KnownBrowser.FIREFOX.packageName,
|
||||||
|
Browsers.KnownBrowser.CHROME.packageName,
|
||||||
|
Browsers.KnownBrowser.SAMSUNG_INTERNET.packageName,
|
||||||
|
Browsers.KnownBrowser.DUCKDUCKGO.packageName,
|
||||||
|
Browsers.KnownBrowser.REFERENCE_BROWSER.packageName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
BrowsersCache.resetAll()
|
||||||
|
|
||||||
|
val updatedBrowserList = BrowsersCache.all(testContext)
|
||||||
|
assertEquals(7, updatedBrowserList.installedBrowsers.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resetting the cache should empty it`() {
|
||||||
|
BrowsersCache.resetAll()
|
||||||
|
|
||||||
|
BrowsersCache.all(testContext)
|
||||||
|
|
||||||
|
assertNotNull(BrowsersCache.cachedBrowsers)
|
||||||
|
|
||||||
|
BrowsersCache.resetAll()
|
||||||
|
|
||||||
|
assertNull(BrowsersCache.cachedBrowsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pretendBrowsersAreInstalled was taken, verbatim, from a-c.
|
||||||
|
// See support/utils/src/test/java/mozilla/components/support/utils/BrowsersTest.kt
|
||||||
|
private fun pretendBrowsersAreInstalled(
|
||||||
|
browsers: List<String> = listOf(),
|
||||||
|
defaultBrowser: String? = null,
|
||||||
|
url: String = "http://www.mozilla.org",
|
||||||
|
browsersExported: Boolean = true,
|
||||||
|
defaultBrowserExported: Boolean = true
|
||||||
|
) {
|
||||||
|
val packageManager = testContext.packageManager
|
||||||
|
val shadow = shadowOf(packageManager)
|
||||||
|
|
||||||
|
browsers.forEach { packageName ->
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.`package` = packageName
|
||||||
|
intent.data = Uri.parse(url)
|
||||||
|
|
||||||
|
val packageInfo = PackageInfo().apply {
|
||||||
|
this.packageName = packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
shadow.installPackage(packageInfo)
|
||||||
|
|
||||||
|
val activityInfo = ActivityInfo().apply {
|
||||||
|
exported = browsersExported
|
||||||
|
this.packageName = packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolveInfo = ResolveInfo().apply {
|
||||||
|
resolvePackageName = packageName
|
||||||
|
this.activityInfo = activityInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
shadow.addResolveInfoForIntent(intent, resolveInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultBrowser != null) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = Uri.parse(url)
|
||||||
|
|
||||||
|
val activityInfo = ActivityInfo().apply {
|
||||||
|
exported = defaultBrowserExported
|
||||||
|
packageName = defaultBrowser
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolveInfo = ResolveInfo().apply {
|
||||||
|
resolvePackageName = defaultBrowser
|
||||||
|
this.activityInfo = activityInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
shadow.addResolveInfoForIntent(intent, resolveInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue