Skip to content

Commit

Permalink
For mozilla-mobile#16032: Support installing recommended add-ons from…
Browse files Browse the repository at this point in the history
… AMO
  • Loading branch information
csadilek committed Dec 2, 2020
1 parent 26051f7 commit 3722033
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,26 @@
package org.mozilla.fenix

import android.content.Context
import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ui.robots.appContext
import java.lang.ref.WeakReference

/**
* This class overrides the application's request interceptor to
* deactivate the FxA web channel
* which is not supported on the staging servers.
*/

class AppRequestInterceptor(private val context: Context) : RequestInterceptor {

private var navController: WeakReference<NavController>? = null

fun setNavigationController(navController: NavController) {
this.navController = WeakReference(navController)
}

override fun onLoadRequest(
engineSession: EngineSession,
uri: String,
Expand Down
56 changes: 55 additions & 1 deletion app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ package org.mozilla.fenix
import android.content.Context
import android.net.ConnectivityManager
import androidx.core.content.getSystemService
import androidx.navigation.NavController
import mozilla.components.browser.errorpages.ErrorPages
import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isOnline
import java.lang.ref.WeakReference

class AppRequestInterceptor(
private val context: Context
) : RequestInterceptor {

private var navController: WeakReference<NavController>? = null

fun setNavigationController(navController: NavController) {
this.navController = WeakReference(navController)
}

class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
override fun onLoadRequest(
engineSession: EngineSession,
uri: String,
Expand All @@ -26,6 +37,11 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
isDirectNavigation: Boolean,
isSubframeRequest: Boolean
): RequestInterceptor.InterceptionResponse? {

interceptAmoRequest(uri, isSameDomain, hasUserGesture)?.let { response ->
return response
}

return context.components.services.appLinksInterceptor
.onLoadRequest(
engineSession,
Expand Down Expand Up @@ -59,6 +75,42 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
return RequestInterceptor.ErrorResponse.Uri(errorPageUri)
}

/**
* Checks if the provided [uri] is a request to install an add-on from addons.mozilla.org and
* redirects to Add-ons Manager to trigger installation if needed.
*
* @return [RequestInterceptor.InterceptionResponse.Deny] when installation was triggered and
* the original request can be skipped, otherwise null to continue loading the page.
*/
private fun interceptAmoRequest(
uri: String,
isSameDomain: Boolean,
hasUserGesture: Boolean
): RequestInterceptor.InterceptionResponse? {
// First we execute a quick check to see if this is a request we're interested in i.e. a
// request triggered by the user and coming from AMO.
if (hasUserGesture && isSameDomain && uri.startsWith(AMO_BASE_URL)) {

// Check if this is a request to install an add-on.
val matchResult = AMO_INSTALL_URL_REGEX.toRegex().matchEntire(uri)
if (matchResult != null) {

// Navigate and trigger add-on installation.
matchResult.groupValues.getOrNull(1)?.let { addonId ->
navController?.get()?.navigate(
NavGraphDirections.actionGlobalAddonsManagementFragment(addonId)
)

// We've redirected to the add-ons management fragment, skip original request.
return RequestInterceptor.InterceptionResponse.Deny
}
}
}

// In all other case we let the original request proceed.
return null
}

/**
* Where possible, this will make the error type more accurate by including information not
* available to AC.
Expand Down Expand Up @@ -116,5 +168,7 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
companion object {
internal const val LOW_AND_MEDIUM_RISK_ERROR_PAGES = "low_and_medium_risk_error_pages.html"
internal const val HIGH_RISK_ERROR_PAGES = "high_risk_error_pages.html"
internal const val AMO_BASE_URL = "https://addons.mozilla.org"
internal const val AMO_INSTALL_URL_REGEX = "$AMO_BASE_URL/android/downloads/file/([^\\s]+)/([^\\s]+\\.xpi)"
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/org/mozilla/fenix/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {

startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)

components.core.requestInterceptor.setNavigationController(navHost.navController)

StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.VisibleForTesting
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_add_ons_management.*
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.*
Expand All @@ -28,6 +30,7 @@ import mozilla.components.feature.addons.ui.AddonsManagerAdapter
import mozilla.components.feature.addons.ui.PermissionsDialogFragment
import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
Expand All @@ -44,10 +47,21 @@ import java.util.concurrent.CancellationException
@Suppress("TooManyFunctions", "LargeClass")
class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) {

private val args by navArgs<AddonsManagementFragmentArgs>()

/**
* Whether or not an add-on installation is in progress.
*/
private var isInstallationInProgress = false

private var installExternalAddonComplete: Boolean
set(value) {
arguments?.putBoolean(BUNDLE_KEY_INSTALL_EXTERNAL_ADDON_COMPLETE, value)
}
get() {
return arguments?.getBoolean(BUNDLE_KEY_INSTALL_EXTERNAL_ADDON_COMPLETE, false) ?: false
}

private var adapter: AddonsManagerAdapter? = null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -82,9 +96,13 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
val recyclerView = view.add_ons_list
recyclerView.layoutManager = LinearLayoutManager(requireContext())
val shouldRefresh = adapter != null

// If the fragment was launched to install an "external" add-on from AMO, we deactivate
// the cache to get the most up-to-date list of add-ons to match against.
val allowCache = args.installAddonId == null || installExternalAddonComplete
lifecycleScope.launch(IO) {
try {
val addons = requireContext().components.addonManager.getAddons()
val addons = requireContext().components.addonManager.getAddons(allowCache = allowCache)
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
if (!shouldRefresh) {
Expand All @@ -103,6 +121,12 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
if (shouldRefresh) {
adapter?.updateAddons(addons)
}

args.installAddonId?.let { addonIn ->
if (!installExternalAddonComplete) {
installExternalAddon(addons, addonIn)
}
}
}
}
} catch (e: AddonManagerException) {
Expand All @@ -121,6 +145,30 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
}
}

@VisibleForTesting
internal fun installExternalAddon(supportedAddons: List<Addon>, installAddonId: String) {
val addonToInstall = supportedAddons.find { it.downloadId == installAddonId }
if (addonToInstall == null) {
showErrorSnackBar(getString(R.string.addon_not_supported_error))
} else {
if (addonToInstall.isInstalled()) {
showErrorSnackBar(getString(R.string.addon_already_installed))
} else {
showPermissionDialog(addonToInstall)
}
}
installExternalAddonComplete = true
}

@VisibleForTesting
internal fun showErrorSnackBar(text: String) {
runIfFragmentIsAttached {
view?.let {
showSnackBar(it, text, FenixSnackbar.LENGTH_LONG)
}
}
}

private fun createAddonStyle(context: Context): AddonsManagerAdapter.Style {
return AddonsManagerAdapter.Style(
sectionsTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
Expand All @@ -144,7 +192,8 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
as? AddonInstallationDialogFragment != null
}

private fun showPermissionDialog(addon: Addon) {
@VisibleForTesting
internal fun showPermissionDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) {
val dialog = PermissionsDialogFragment.newInstance(
addon = addon,
Expand Down Expand Up @@ -278,5 +327,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
companion object {
private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
private const val INSTALLATION_DIALOG_FRAGMENT_TAG = "ADDONS_INSTALLATION_DIALOG_FRAGMENT"
private const val BUNDLE_KEY_INSTALL_EXTERNAL_ADDON_COMPLETE = "INSTALL_EXTERNAL_ADDON_COMPLETE"
}
}
4 changes: 2 additions & 2 deletions app/src/main/java/org/mozilla/fenix/addons/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import org.mozilla.fenix.components.FenixSnackbar
* @param view A [View] used to determine a parent for the [FenixSnackbar].
* @param text The text to display in the [FenixSnackbar].
*/
internal fun showSnackBar(view: View, text: String) {
internal fun showSnackBar(view: View, text: String, duration: Int = FenixSnackbar.LENGTH_SHORT) {
FenixSnackbar.make(
view = view,
duration = FenixSnackbar.LENGTH_SHORT,
duration = duration,
isDisplayedWithBrowserToolbar = true
)
.setText(text)
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/java/org/mozilla/fenix/components/Core.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class Core(
*/
val engine: Engine by lazyMonitored {
val defaultSettings = DefaultSettings(
requestInterceptor = AppRequestInterceptor(context),
requestInterceptor = requestInterceptor,
remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M,
testingModeEnabled = false,
Expand Down Expand Up @@ -141,6 +141,15 @@ class Core(
}
}

/**
* Passed to [engine] to intercept requests for app links,
* and various features triggered by page load requests.
*
* NB: This does not need to be lazy as it is initialized
* with the engine on startup.
*/
val requestInterceptor = AppRequestInterceptor(context)

/**
* [Client] implementation to be used for code depending on `concept-fetch``
*/
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/res/navigation/nav_graph.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@
app:destination="@id/bookmarkEditFragment" />
<action
android:id="@+id/action_global_addonsManagementFragment"
app:destination="@id/addons_management_graph" />
app:destination="@id/addons_management_graph">
<argument
android:name="installAddonId"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
</action>
<action
android:id="@+id/action_global_trackingProtectionFragment"
app:destination="@id/trackingProtectionFragment" />
Expand Down Expand Up @@ -866,6 +872,11 @@
<action
android:id="@+id/action_addonsManagementFragment_to_notYetSupportedAddonFragment"
app:destination="@id/notYetSupportedAddonFragment" />
<argument
android:name="installAddonId"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/installedAddonDetailsFragment"
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,12 @@
<!-- Toast shown after confirming the custom add-on collection configuration -->
<string name="toast_customize_addon_collection_done">Add-on collection modified. Quitting the application to apply changes…</string>

<!-- Add-on Installation from AMO-->
<!-- Error displayed when user attempts to install an add-on from AMO (addons.mozilla.org) that is not supported -->
<string name="addon_not_supported_error">Add-on is not supported</string>
<!-- Error displayed when user attempts to install an add-on from AMO (addons.mozilla.org) that is already installed -->
<string name="addon_already_installed">Add-on is already installed</string>

<!-- Account Preferences -->
<!-- Preference for triggering sync -->
<string name="preferences_sync_now">Sync now</string>
Expand Down

0 comments on commit 3722033

Please sign in to comment.