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