Merge remote-tracking branch 'upstream/master' into fork
commit
e13b236588
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,99 @@
|
|||||||
|
/* 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.components
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import com.google.android.play.core.ktx.launchReview
|
||||||
|
import com.google.android.play.core.ktx.requestReview
|
||||||
|
import com.google.android.play.core.review.ReviewManagerFactory
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that describes the settings needed to track the Review Prompt.
|
||||||
|
*/
|
||||||
|
interface ReviewSettings {
|
||||||
|
var numberOfAppLaunches: Int
|
||||||
|
val isDefaultBrowser: Boolean
|
||||||
|
var lastReviewPromptTimeInMillis: Long
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps `Settings` to conform to `ReviewSettings`.
|
||||||
|
*/
|
||||||
|
class FenixReviewSettings(
|
||||||
|
val settings: Settings
|
||||||
|
) : ReviewSettings {
|
||||||
|
override var numberOfAppLaunches: Int
|
||||||
|
get() = settings.numberOfAppLaunches
|
||||||
|
set(value) { settings.numberOfAppLaunches = value }
|
||||||
|
override val isDefaultBrowser: Boolean
|
||||||
|
get() = settings.isDefaultBrowser()
|
||||||
|
override var lastReviewPromptTimeInMillis: Long
|
||||||
|
get() = settings.lastReviewPromptTimeInMillis
|
||||||
|
set(value) { settings.lastReviewPromptTimeInMillis = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls the Review Prompt behavior.
|
||||||
|
*/
|
||||||
|
class ReviewPromptController(
|
||||||
|
private val context: Context,
|
||||||
|
private val reviewSettings: ReviewSettings,
|
||||||
|
private val timeNowInMillis: () -> Long = { System.currentTimeMillis() },
|
||||||
|
private val tryPromptReview: suspend (Activity) -> Unit = {
|
||||||
|
val manager = ReviewManagerFactory.create(context)
|
||||||
|
val reviewInfo = manager.requestReview()
|
||||||
|
|
||||||
|
withContext(Main) {
|
||||||
|
manager.launchReview(it, reviewInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
@Volatile var reviewPromptIsReady = false
|
||||||
|
|
||||||
|
suspend fun promptReview(activity: Activity) {
|
||||||
|
if (shouldShowPrompt()) {
|
||||||
|
tryPromptReview(activity)
|
||||||
|
reviewSettings.lastReviewPromptTimeInMillis = timeNowInMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trackApplicationLaunch() {
|
||||||
|
reviewSettings.numberOfAppLaunches = reviewSettings.numberOfAppLaunches + 1
|
||||||
|
// We only want to show the the prompt after we've finished "launching" the application.
|
||||||
|
reviewPromptIsReady = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
fun shouldShowPrompt(): Boolean {
|
||||||
|
if (!reviewPromptIsReady) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
// We only want to try to show it once to avoid unnecessary disk reads
|
||||||
|
reviewPromptIsReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reviewSettings.isDefaultBrowser) { return false }
|
||||||
|
|
||||||
|
val hasOpenedFiveTimes = reviewSettings.numberOfAppLaunches >= NUMBER_OF_LAUNCHES_REQUIRED
|
||||||
|
val now = timeNowInMillis()
|
||||||
|
val apprxFourMonthsAgo = now - (APPRX_MONTH_IN_MILLIS * NUMBER_OF_MONTHS_TO_PASS)
|
||||||
|
val lastPrompt = reviewSettings.lastReviewPromptTimeInMillis
|
||||||
|
val hasNotBeenPromptedLastFourMonths = lastPrompt == 0L || lastPrompt <= apprxFourMonthsAgo
|
||||||
|
|
||||||
|
return hasOpenedFiveTimes && hasNotBeenPromptedLastFourMonths
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val APPRX_MONTH_IN_MILLIS: Long = 1000L * 60L * 60L * 24L * 30L
|
||||||
|
private const val NUMBER_OF_LAUNCHES_REQUIRED = 5
|
||||||
|
private const val NUMBER_OF_MONTHS_TO_PASS = 4
|
||||||
|
}
|
||||||
|
}
|
@ -1,96 +0,0 @@
|
|||||||
/* 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.components
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import mozilla.components.feature.top.sites.TopSite
|
|
||||||
import mozilla.components.feature.top.sites.TopSiteStorage
|
|
||||||
import mozilla.components.support.locale.LocaleManager
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.ext.observeOnce
|
|
||||||
import org.mozilla.fenix.ext.settings
|
|
||||||
import org.mozilla.fenix.settings.SupportUtils
|
|
||||||
import org.mozilla.fenix.settings.advanced.getSelectedLocale
|
|
||||||
import org.mozilla.fenix.utils.Mockable
|
|
||||||
|
|
||||||
@Mockable
|
|
||||||
class TopSiteStorage(private val context: Context) {
|
|
||||||
var cachedTopSites = listOf<TopSite>()
|
|
||||||
|
|
||||||
val storage by lazy {
|
|
||||||
TopSiteStorage(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
addDefaultTopSites()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new [TopSite].
|
|
||||||
*/
|
|
||||||
fun addTopSite(title: String, url: String, isDefault: Boolean = false) {
|
|
||||||
storage.addTopSite(title, url, isDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a [LiveData] list of all the [TopSite] instances.
|
|
||||||
*/
|
|
||||||
fun getTopSites(): LiveData<List<TopSite>> {
|
|
||||||
return storage.getTopSites().asLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the given [TopSite].
|
|
||||||
*/
|
|
||||||
fun removeTopSite(topSite: TopSite) {
|
|
||||||
storage.removeTopSite(topSite)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addDefaultTopSites() {
|
|
||||||
val topSiteCandidates = mutableListOf<Pair<String, String>>()
|
|
||||||
if (!context.settings().defaultTopSitesAdded) {
|
|
||||||
topSiteCandidates.add(
|
|
||||||
Pair(
|
|
||||||
context.getString(R.string.default_top_site_google),
|
|
||||||
SupportUtils.GOOGLE_URL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (LocaleManager.getSelectedLocale(context).language == "en") {
|
|
||||||
topSiteCandidates.add(
|
|
||||||
Pair(
|
|
||||||
context.getString(R.string.pocket_pinned_top_articles),
|
|
||||||
SupportUtils.POCKET_TRENDING_URL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
topSiteCandidates.add(
|
|
||||||
Pair(
|
|
||||||
context.getString(R.string.default_top_site_wikipedia),
|
|
||||||
SupportUtils.WIKIPEDIA_URL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
topSiteCandidates.forEach { (title, url) ->
|
|
||||||
addTopSite(title, url, isDefault = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.settings().defaultTopSitesAdded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prefetch() {
|
|
||||||
getTopSites().observeOnce {
|
|
||||||
cachedTopSites = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
/* 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.components.metrics
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.annotation.VisibleForTesting.PRIVATE
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleObserver
|
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
|
||||||
import mozilla.components.support.utils.SafeIntent
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks how the application was opened through [Event.AppOpenedAllSourceStartup].
|
|
||||||
* We only considered to be "opened" if it received an intent and the app was in the background.
|
|
||||||
*/
|
|
||||||
class AppAllSourceStartTelemetry(private val metrics: MetricController) : LifecycleObserver {
|
|
||||||
|
|
||||||
// default value is true to capture the first launch of the application
|
|
||||||
private var wasApplicationInBackground = true
|
|
||||||
|
|
||||||
init {
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun receivedIntentInExternalAppBrowserActivity(safeIntent: SafeIntent) {
|
|
||||||
setAppOpenedAllSourceFromIntent(safeIntent, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun receivedIntentInHomeActivity(safeIntent: SafeIntent) {
|
|
||||||
setAppOpenedAllSourceFromIntent(safeIntent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAppOpenedAllSourceFromIntent(intent: SafeIntent, isExternalAppBrowserActivity: Boolean) {
|
|
||||||
if (!wasApplicationInBackground) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val source = when {
|
|
||||||
isExternalAppBrowserActivity -> Event.AppOpenedAllSourceStartup.Source.CUSTOM_TAB
|
|
||||||
intent.isLauncherIntent -> Event.AppOpenedAllSourceStartup.Source.APP_ICON
|
|
||||||
intent.action == Intent.ACTION_VIEW -> Event.AppOpenedAllSourceStartup.Source.LINK
|
|
||||||
else -> Event.AppOpenedAllSourceStartup.Source.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics.track(Event.AppOpenedAllSourceStartup(source))
|
|
||||||
|
|
||||||
wasApplicationInBackground = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
|
||||||
@VisibleForTesting(otherwise = PRIVATE)
|
|
||||||
fun onApplicationOnStop() {
|
|
||||||
wasApplicationInBackground = true
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,146 @@
|
|||||||
|
/* 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.components.metrics
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleObserver
|
||||||
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import mozilla.components.support.utils.SafeIntent
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.APP_ICON
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.CUSTOM_TAB
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.LINK
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.UNKNOWN
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.ERROR
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.HOT
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.COLD
|
||||||
|
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.WARM
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks application startup source, type, and whether or not activity has savedInstance to restore
|
||||||
|
* the activity from. Sample metric = [source = COLD, type = APP_ICON, hasSavedInstance = false]
|
||||||
|
* The basic idea is to collect these metrics from different phases of startup through
|
||||||
|
* [AppAllStartup] and finally report them on Activity's onResume() function.
|
||||||
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
class AppStartupTelemetry(private val metrics: MetricController) : LifecycleObserver {
|
||||||
|
|
||||||
|
init {
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isMetricRecordedSinceAppWasForegrounded = false
|
||||||
|
private var wasAppCreateCalledBeforeActivityCreate = false
|
||||||
|
|
||||||
|
private var onCreateData: AppAllStartup? = null
|
||||||
|
private var onRestartData: Pair<Type, Boolean?>? = null
|
||||||
|
private var onNewIntentData: Source? = null
|
||||||
|
|
||||||
|
fun onFenixApplicationOnCreate() {
|
||||||
|
wasAppCreateCalledBeforeActivityCreate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onHomeActivityOnCreate(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) {
|
||||||
|
setOnCreateData(safeIntent, hasSavedInstanceState, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onExternalAppBrowserOnCreate(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) {
|
||||||
|
setOnCreateData(safeIntent, hasSavedInstanceState, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onHomeActivityOnRestart() {
|
||||||
|
// we are not setting [Source] in this method since source is derived from an intent.
|
||||||
|
// therefore source gets set in onNewIntent().
|
||||||
|
onRestartData = Pair(HOT, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onHomeActivityOnNewIntent(safeIntent: SafeIntent) {
|
||||||
|
// we are only setting [Source] in this method since source is derived from an intent].
|
||||||
|
// other metric fields are set in onRestart()
|
||||||
|
onNewIntentData = getStartupSourceFromIntent(safeIntent, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOnCreateData(
|
||||||
|
safeIntent: SafeIntent,
|
||||||
|
hasSavedInstanceState: Boolean,
|
||||||
|
isExternalAppBrowserActivity: Boolean
|
||||||
|
) {
|
||||||
|
onCreateData = AppAllStartup(
|
||||||
|
getStartupSourceFromIntent(safeIntent, isExternalAppBrowserActivity),
|
||||||
|
getAppStartupType(),
|
||||||
|
hasSavedInstanceState
|
||||||
|
)
|
||||||
|
wasAppCreateCalledBeforeActivityCreate = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAppStartupType(): Type {
|
||||||
|
return if (wasAppCreateCalledBeforeActivityCreate) COLD else WARM
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStartupSourceFromIntent(
|
||||||
|
intent: SafeIntent,
|
||||||
|
isExternalAppBrowserActivity: Boolean
|
||||||
|
): Source {
|
||||||
|
return when {
|
||||||
|
// since the intent action is same (ACTION_VIEW) for both CUSTOM_TAB and LINK.
|
||||||
|
// we have to make sure that we are checking for CUSTOM_TAB condition first as this
|
||||||
|
// check does not rely on intent action
|
||||||
|
isExternalAppBrowserActivity -> CUSTOM_TAB
|
||||||
|
intent.isLauncherIntent -> APP_ICON
|
||||||
|
intent.action == Intent.ACTION_VIEW -> LINK
|
||||||
|
// one of the unknown case is app switcher, where we go to the recent tasks to launch
|
||||||
|
// Fenix.
|
||||||
|
else -> UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason we record metric on resume is because we need to wait for onNewIntent(), and
|
||||||
|
* we are not guaranteed that onNewIntent() will be called before or after onStart() / onRestart().
|
||||||
|
* However we are guaranteed onResume() will be called after onNewIntent() and onStart(). Source:
|
||||||
|
* https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
|
||||||
|
*/
|
||||||
|
fun onHomeActivityOnResume() {
|
||||||
|
recordMetric()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recordMetric() {
|
||||||
|
if (!isMetricRecordedSinceAppWasForegrounded) {
|
||||||
|
val appAllStartup: AppAllStartup = if (onCreateData != null) {
|
||||||
|
onCreateData!!
|
||||||
|
} else {
|
||||||
|
mergeOnRestartAndOnNewIntentIntoStartup()
|
||||||
|
}
|
||||||
|
metrics.track(appAllStartup)
|
||||||
|
isMetricRecordedSinceAppWasForegrounded = true
|
||||||
|
}
|
||||||
|
// we don't want any weird previous states to persist on our next metric record.
|
||||||
|
onCreateData = null
|
||||||
|
onNewIntentData = null
|
||||||
|
onRestartData = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mergeOnRestartAndOnNewIntentIntoStartup(): AppAllStartup {
|
||||||
|
return AppAllStartup(
|
||||||
|
onNewIntentData ?: UNKNOWN,
|
||||||
|
onRestartData?.first ?: ERROR,
|
||||||
|
onRestartData?.second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
fun onApplicationOnStop() {
|
||||||
|
// application was backgrounded, we need to record the new metric type if
|
||||||
|
// application was to come to foreground again.
|
||||||
|
// Therefore we set the isMetricRecorded flag to false.
|
||||||
|
isMetricRecordedSinceAppWasForegrounded = false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/* 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.ext
|
||||||
|
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.library.downloads.DownloadItem
|
||||||
|
|
||||||
|
// While this looks complex, it's actually pretty simple.
|
||||||
|
@SuppressWarnings("ComplexMethod")
|
||||||
|
fun DownloadItem.getIcon(): Int {
|
||||||
|
fun getIconCornerCases(fileName: String?): Int {
|
||||||
|
return when {
|
||||||
|
fileName?.endsWith("apk") == true -> R.drawable.ic_file_type_apk
|
||||||
|
fileName?.endsWith("zip") == true -> R.drawable.ic_file_type_zip
|
||||||
|
else -> R.drawable.ic_file_type_default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkForApplicationArchiveSubtypes(contentType: String): Int? {
|
||||||
|
return when {
|
||||||
|
contentType.contains("rar") -> R.drawable.ic_file_type_zip
|
||||||
|
contentType.contains("zip") -> R.drawable.ic_file_type_zip
|
||||||
|
contentType.contains("7z") -> R.drawable.ic_file_type_zip
|
||||||
|
contentType.contains("tar") -> R.drawable.ic_file_type_zip
|
||||||
|
contentType.contains("freearc") -> R.drawable.ic_file_type_zip
|
||||||
|
contentType.contains("octet-stream") -> null
|
||||||
|
contentType.contains("vnd.android.package-archive") -> null
|
||||||
|
else -> R.drawable.ic_file_type_document
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIconFromContentType(contentType: String): Int? {
|
||||||
|
return when {
|
||||||
|
contentType.contains("image/") -> R.drawable.ic_file_type_image
|
||||||
|
contentType.contains("audio/") -> R.drawable.ic_file_type_audio_note
|
||||||
|
contentType.contains("video/") -> R.drawable.ic_file_type_video
|
||||||
|
contentType.contains("application/") -> checkForApplicationArchiveSubtypes(contentType)
|
||||||
|
contentType.contains("text/") -> R.drawable.ic_file_type_document
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentType?.let { contentType ->
|
||||||
|
getIconFromContentType(contentType)
|
||||||
|
} ?: getIconCornerCases(fileName)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
/* 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.ext
|
||||||
|
|
||||||
|
import android.widget.EditText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places cursor at the end of an EditText.
|
||||||
|
*/
|
||||||
|
fun EditText.placeCursorAtEnd() {
|
||||||
|
this.text?.length?.let { setSelection(it, it) }
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/* 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.ext
|
||||||
|
|
||||||
|
import org.mozilla.fenix.library.downloads.DownloadItem
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a List of DownloadItems to verify whether items
|
||||||
|
* on that list are present on the disk or not. If a user has
|
||||||
|
* deleted the downloaded item it should not show on the downloaded
|
||||||
|
* list.
|
||||||
|
*/
|
||||||
|
fun List<DownloadItem>.filterNotExistsOnDisk(): List<DownloadItem> {
|
||||||
|
return this.filter {
|
||||||
|
File(it.filePath).exists()
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
/* 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.ext
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observe a LiveData once and unregister from it as soon as the live data returns a value
|
|
||||||
*/
|
|
||||||
fun <T> LiveData<T>.observeOnce(observer: (T) -> Unit) {
|
|
||||||
observeForever(object : Observer<T> {
|
|
||||||
override fun onChanged(value: T) {
|
|
||||||
removeObserver(this)
|
|
||||||
observer(value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -0,0 +1,57 @@
|
|||||||
|
/* 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.home.sessioncontrol.viewholders
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import kotlinx.android.synthetic.main.component_top_sites_pager.view.*
|
||||||
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSitesPagerAdapter
|
||||||
|
|
||||||
|
class TopSitePagerViewHolder(
|
||||||
|
view: View,
|
||||||
|
interactor: TopSiteInteractor
|
||||||
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
private val topSitesPagerAdapter = TopSitesPagerAdapter(interactor)
|
||||||
|
private val pageIndicator = view.page_indicator
|
||||||
|
|
||||||
|
private val topSitesPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
pageIndicator.setSelection(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.top_sites_pager.apply {
|
||||||
|
adapter = topSitesPagerAdapter
|
||||||
|
registerOnPageChangeCallback(topSitesPageChangeCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(topSites: List<TopSite>) {
|
||||||
|
topSitesPagerAdapter.updateData(topSites)
|
||||||
|
|
||||||
|
// Don't show any page indicator if there is only 1 page.
|
||||||
|
val numPages = if (topSites.size > TOP_SITES_PER_PAGE) {
|
||||||
|
TOP_SITES_MAX_PAGE_SIZE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pageIndicator.isVisible = numPages > 1
|
||||||
|
pageIndicator.setSize(numPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.component_top_sites_pager
|
||||||
|
const val TOP_SITES_MAX_PAGE_SIZE = 2
|
||||||
|
const val TOP_SITES_PER_PAGE = 8
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
/* 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.home.sessioncontrol.viewholders.topsites
|
||||||
|
|
||||||
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
|
import mozilla.components.feature.top.sites.view.TopSitesView
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentAction
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentStore
|
||||||
|
|
||||||
|
class DefaultTopSitesView(
|
||||||
|
val store: HomeFragmentStore
|
||||||
|
) : TopSitesView {
|
||||||
|
|
||||||
|
override fun displayTopSites(topSites: List<TopSite>) {
|
||||||
|
store.dispatch(HomeFragmentAction.TopSitesChange(topSites))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
/* 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.home.sessioncontrol.viewholders.topsites
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.MarginLayoutParamsCompat
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pager indicator widget to display the number of pages and the current selected page.
|
||||||
|
*/
|
||||||
|
class PagerIndicator : LinearLayout {
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
private var selectedIndex = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the number of pager dots to display.
|
||||||
|
*/
|
||||||
|
fun setSize(size: Int) {
|
||||||
|
if (childCount == size) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selectedIndex >= size) {
|
||||||
|
selectedIndex = size - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllViews()
|
||||||
|
for (i in 0 until size) {
|
||||||
|
val isLast = i == size - 1
|
||||||
|
addView(
|
||||||
|
View(context).apply {
|
||||||
|
setBackgroundResource(R.drawable.pager_dot)
|
||||||
|
isSelected = i == selectedIndex
|
||||||
|
},
|
||||||
|
LayoutParams(dpToPx(DOT_SIZE_IN_DP), dpToPx(DOT_SIZE_IN_DP)).apply {
|
||||||
|
if (!isLast) {
|
||||||
|
MarginLayoutParamsCompat.setMarginEnd(this, dpToPx(DOT_MARGIN))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current selected pager dot.
|
||||||
|
*/
|
||||||
|
fun setSelection(index: Int) {
|
||||||
|
if (selectedIndex == index) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildAt(selectedIndex)?.run {
|
||||||
|
isSelected = false
|
||||||
|
}
|
||||||
|
getChildAt(index)?.run {
|
||||||
|
isSelected = true
|
||||||
|
}
|
||||||
|
selectedIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DOT_SIZE_IN_DP = 6f
|
||||||
|
private const val DOT_MARGIN = 4f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.dpToPx(value: Float): Int =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics).toInt()
|
||||||
|
|
||||||
|
fun View.dpToPx(value: Float): Int = context.dpToPx(value)
|
@ -0,0 +1,40 @@
|
|||||||
|
/* 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.home.sessioncontrol.viewholders.topsites
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder
|
||||||
|
|
||||||
|
class TopSitesPagerAdapter(
|
||||||
|
private val interactor: TopSiteInteractor
|
||||||
|
) : RecyclerView.Adapter<TopSiteViewHolder>() {
|
||||||
|
|
||||||
|
private var topSites: List<List<TopSite>> = listOf()
|
||||||
|
|
||||||
|
fun updateData(topSites: List<TopSite>) {
|
||||||
|
this.topSites = topSites.chunked(TOP_SITES_PER_PAGE)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(TopSiteViewHolder.LAYOUT_ID, parent, false)
|
||||||
|
return TopSiteViewHolder(view, interactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: TopSiteViewHolder, position: Int) {
|
||||||
|
holder.bind(this.topSites[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = this.topSites.size
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TOP_SITES_PER_PAGE = 8
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/* 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.library.downloads
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.mozilla.fenix.library.SelectionHolder
|
||||||
|
import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder
|
||||||
|
|
||||||
|
class DownloadAdapter(
|
||||||
|
private val downloadInteractor: DownloadInteractor
|
||||||
|
) : RecyclerView.Adapter<DownloadsListItemViewHolder>(), SelectionHolder<DownloadItem> {
|
||||||
|
private var downloads: List<DownloadItem> = listOf()
|
||||||
|
private var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
|
||||||
|
override val selectedItems get() = mode.selectedItems
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = downloads.size
|
||||||
|
override fun getItemViewType(position: Int): Int = DownloadsListItemViewHolder.LAYOUT_ID
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsListItemViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||||
|
return DownloadsListItemViewHolder(view, downloadInteractor, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMode(mode: DownloadFragmentState.Mode) {
|
||||||
|
this.mode = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) {
|
||||||
|
holder.bind(downloads[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloads(downloads: List<DownloadItem>) {
|
||||||
|
this.downloads = downloads
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/* 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.library.downloads
|
||||||
|
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
|
||||||
|
interface DownloadController {
|
||||||
|
fun handleOpen(item: DownloadItem, mode: BrowsingMode? = null)
|
||||||
|
fun handleBackPressed(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultDownloadController(
|
||||||
|
private val store: DownloadFragmentStore,
|
||||||
|
private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit
|
||||||
|
) : DownloadController {
|
||||||
|
override fun handleOpen(item: DownloadItem, mode: BrowsingMode?) {
|
||||||
|
openToFileManager(item, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleBackPressed(): Boolean {
|
||||||
|
return if (store.state.mode is DownloadFragmentState.Mode.Editing) {
|
||||||
|
store.dispatch(DownloadFragmentAction.ExitEditMode)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
/* 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.library.downloads
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import kotlinx.android.synthetic.main.fragment_downloads.view.*
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import mozilla.components.browser.state.state.content.DownloadState
|
||||||
|
import mozilla.components.feature.downloads.AbstractFetchDownloadService
|
||||||
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
|
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.filterNotExistsOnDisk
|
||||||
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
|
import org.mozilla.fenix.library.LibraryPageFragment
|
||||||
|
|
||||||
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
|
class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHandler {
|
||||||
|
private lateinit var downloadStore: DownloadFragmentStore
|
||||||
|
private lateinit var downloadView: DownloadView
|
||||||
|
private lateinit var downloadInteractor: DownloadInteractor
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_downloads, container, false)
|
||||||
|
|
||||||
|
val items = requireComponents.core.store.state.downloads.map {
|
||||||
|
DownloadItem(
|
||||||
|
it.value.id.toString(),
|
||||||
|
it.value.fileName,
|
||||||
|
it.value.filePath,
|
||||||
|
it.value.contentLength.toString(),
|
||||||
|
it.value.contentType,
|
||||||
|
it.value.status
|
||||||
|
)
|
||||||
|
}.filter {
|
||||||
|
it.status == DownloadState.Status.COMPLETED
|
||||||
|
}.filterNotExistsOnDisk()
|
||||||
|
|
||||||
|
downloadStore = StoreProvider.get(this) {
|
||||||
|
DownloadFragmentStore(
|
||||||
|
DownloadFragmentState(
|
||||||
|
items = items,
|
||||||
|
mode = DownloadFragmentState.Mode.Normal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadController: DownloadController = DefaultDownloadController(
|
||||||
|
downloadStore,
|
||||||
|
::openItem
|
||||||
|
)
|
||||||
|
downloadInteractor = DownloadInteractor(
|
||||||
|
downloadController
|
||||||
|
)
|
||||||
|
downloadView = DownloadView(view.downloadsLayout, downloadInteractor)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override val selectedItems get() = downloadStore.state.mode.selectedItems
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
requireComponents.analytics.metrics.track(Event.HistoryOpened)
|
||||||
|
|
||||||
|
setHasOptionsMenu(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
consumeFrom(downloadStore) {
|
||||||
|
downloadView.update(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
showToolbar(getString(R.string.library_downloads))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return downloadView.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openItem(item: DownloadItem, mode: BrowsingMode? = null) {
|
||||||
|
|
||||||
|
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }
|
||||||
|
context?.let {
|
||||||
|
AbstractFetchDownloadService.openFile(
|
||||||
|
context = it,
|
||||||
|
contentType = item.contentType,
|
||||||
|
filePath = item.filePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/* 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.library.downloads
|
||||||
|
|
||||||
|
import mozilla.components.browser.state.state.content.DownloadState
|
||||||
|
import mozilla.components.lib.state.Action
|
||||||
|
import mozilla.components.lib.state.State
|
||||||
|
import mozilla.components.lib.state.Store
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a history entry
|
||||||
|
* @property id Unique id of the download item
|
||||||
|
* @property fileName File name of the download item
|
||||||
|
* @property filePath Full path of the download item
|
||||||
|
* @property size The size in bytes of the download item
|
||||||
|
* @property contentType The type of file the download is
|
||||||
|
*/
|
||||||
|
data class DownloadItem(
|
||||||
|
val id: String,
|
||||||
|
val fileName: String?,
|
||||||
|
val filePath: String,
|
||||||
|
val size: String,
|
||||||
|
val contentType: String?,
|
||||||
|
val status: DownloadState.Status
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Store] for holding the [DownloadFragmentState] and applying [DownloadFragmentAction]s.
|
||||||
|
*/
|
||||||
|
class DownloadFragmentStore(initialState: DownloadFragmentState) :
|
||||||
|
Store<DownloadFragmentState, DownloadFragmentAction>(initialState, ::downloadStateReducer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to dispatch through the `DownloadStore` to modify `DownloadState` through the reducer.
|
||||||
|
*/
|
||||||
|
sealed class DownloadFragmentAction : Action {
|
||||||
|
object ExitEditMode : DownloadFragmentAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state for the Download Screen
|
||||||
|
* @property items List of DownloadItem to display
|
||||||
|
* @property mode Current Mode of Download
|
||||||
|
*/
|
||||||
|
data class DownloadFragmentState(
|
||||||
|
val items: List<DownloadItem>,
|
||||||
|
val mode: Mode
|
||||||
|
) : State {
|
||||||
|
sealed class Mode {
|
||||||
|
open val selectedItems = emptySet<DownloadItem>()
|
||||||
|
|
||||||
|
object Normal : Mode()
|
||||||
|
data class Editing(override val selectedItems: Set<DownloadItem>) : DownloadFragmentState.Mode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DownloadState Reducer.
|
||||||
|
*/
|
||||||
|
private fun downloadStateReducer(
|
||||||
|
state: DownloadFragmentState,
|
||||||
|
action: DownloadFragmentAction
|
||||||
|
): DownloadFragmentState {
|
||||||
|
return when (action) {
|
||||||
|
is DownloadFragmentAction.ExitEditMode -> state.copy(mode = DownloadFragmentState.Mode.Normal)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/* 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.library.downloads
|
||||||
|
/**
|
||||||
|
* Interactor for the download screen
|
||||||
|
* Provides implementations for the DownloadViewInteractor
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
|
class DownloadInteractor(
|
||||||
|
private val downloadController: DownloadController
|
||||||
|
) : DownloadViewInteractor {
|
||||||
|
override fun open(item: DownloadItem) {
|
||||||
|
downloadController.handleOpen(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun select(item: DownloadItem) { /* noop */ }
|
||||||
|
|
||||||
|
override fun deselect(item: DownloadItem) { /* noop */ }
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return downloadController.handleBackPressed()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/* 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.library.downloads
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import kotlinx.android.synthetic.main.component_downloads.*
|
||||||
|
import kotlinx.android.synthetic.main.component_downloads.view.*
|
||||||
|
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.library.LibraryPageView
|
||||||
|
import org.mozilla.fenix.library.SelectionInteractor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the DownloadViewInteractor. This interface is implemented by objects that want
|
||||||
|
* to respond to user interaction on the DownloadView
|
||||||
|
*/
|
||||||
|
interface DownloadViewInteractor : SelectionInteractor<DownloadItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on backpressed to exit edit mode
|
||||||
|
*/
|
||||||
|
fun onBackPressed(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View that contains and configures the Downloads List
|
||||||
|
*/
|
||||||
|
class DownloadView(
|
||||||
|
container: ViewGroup,
|
||||||
|
val interactor: DownloadInteractor
|
||||||
|
) : LibraryPageView(container), UserInteractionHandler {
|
||||||
|
|
||||||
|
val view: View = LayoutInflater.from(container.context)
|
||||||
|
.inflate(R.layout.component_downloads, container, true)
|
||||||
|
|
||||||
|
var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
|
||||||
|
private set
|
||||||
|
|
||||||
|
val downloadAdapter = DownloadAdapter(interactor)
|
||||||
|
private val layoutManager = LinearLayoutManager(container.context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.download_list.apply {
|
||||||
|
layoutManager = this@DownloadView.layoutManager
|
||||||
|
adapter = downloadAdapter
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(state: DownloadFragmentState) {
|
||||||
|
|
||||||
|
view.swipe_refresh.isEnabled = false
|
||||||
|
mode = state.mode
|
||||||
|
|
||||||
|
updateEmptyState(state.items.isNotEmpty())
|
||||||
|
|
||||||
|
downloadAdapter.updateMode(state.mode)
|
||||||
|
downloadAdapter.updateDownloads(state.items)
|
||||||
|
|
||||||
|
setUiForNormalMode(
|
||||||
|
context.getString(R.string.library_downloads)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateEmptyState(userHasDownloads: Boolean) {
|
||||||
|
download_list.isVisible = userHasDownloads
|
||||||
|
download_empty_view.isVisible = !userHasDownloads
|
||||||
|
if (!userHasDownloads) {
|
||||||
|
download_empty_view.announceForAccessibility(context.getString(R.string.download_empty_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return interactor.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/* 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.library.downloads.viewholders
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.download_list_item.view.*
|
||||||
|
import kotlinx.android.synthetic.main.library_site_item.view.*
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.ext.hideAndDisable
|
||||||
|
import org.mozilla.fenix.library.SelectionHolder
|
||||||
|
import org.mozilla.fenix.library.downloads.DownloadInteractor
|
||||||
|
import org.mozilla.fenix.library.downloads.DownloadItem
|
||||||
|
import mozilla.components.feature.downloads.toMegabyteString
|
||||||
|
import org.mozilla.fenix.ext.getIcon
|
||||||
|
|
||||||
|
class DownloadsListItemViewHolder(
|
||||||
|
view: View,
|
||||||
|
private val downloadInteractor: DownloadInteractor,
|
||||||
|
private val selectionHolder: SelectionHolder<DownloadItem>
|
||||||
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
private var item: DownloadItem? = null
|
||||||
|
|
||||||
|
fun bind(
|
||||||
|
item: DownloadItem
|
||||||
|
) {
|
||||||
|
itemView.download_layout.visibility = View.VISIBLE
|
||||||
|
itemView.download_layout.titleView.text = item.fileName
|
||||||
|
itemView.download_layout.urlView.text = item.size.toLong().toMegabyteString()
|
||||||
|
|
||||||
|
itemView.download_layout.setSelectionInteractor(item, selectionHolder, downloadInteractor)
|
||||||
|
itemView.download_layout.changeSelected(item in selectionHolder.selectedItems)
|
||||||
|
|
||||||
|
itemView.overflow_menu.hideAndDisable()
|
||||||
|
itemView.favicon.setImageResource(item.getIcon())
|
||||||
|
itemView.favicon.isClickable = false
|
||||||
|
|
||||||
|
this.item = item
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.download_list_item
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
/* 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.onboarding
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
|
|
||||||
|
class OnboardingController(
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
fun handleLearnMoreClicked() {
|
||||||
|
(context as HomeActivity).openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = SupportUtils.getFirefoxAccountSumoUrl(),
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromHome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
/* 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.onboarding
|
||||||
|
|
||||||
|
class OnboardingInteractor(private val onboardingController: OnboardingController) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user clicks the learn more link
|
||||||
|
* @param url the url the suggestion was providing
|
||||||
|
*/
|
||||||
|
fun onLearnMoreClicked() = onboardingController.handleLearnMoreClicked()
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/* 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.perf
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.doOnPreDraw
|
||||||
|
import mozilla.components.support.utils.RunWhenReadyQueue
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class for all functionality related to Visual completeness queue
|
||||||
|
*/
|
||||||
|
class VisualCompletenessQueue(val queue: RunWhenReadyQueue) {
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
val delay = 5000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param containerWeakReference a weak reference to the root view of a view hierarchy. Weak
|
||||||
|
* reference is to avoid memory leak.
|
||||||
|
*/
|
||||||
|
fun attachViewToRunVisualCompletenessQueueLater(containerWeakReference: WeakReference<View>) {
|
||||||
|
containerWeakReference.get()?.doOnPreDraw {
|
||||||
|
// This delay is temporary. We are delaying 5 seconds until the performance
|
||||||
|
// team can locate the real point of visual completeness.
|
||||||
|
it.postDelayed({
|
||||||
|
queue.ready()
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue