Merge remote-tracking branch 'upstream/master' into fork
commit
e8c354f0bb
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,17 @@
|
|||||||
|
/* 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.search.ext
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
|
||||||
|
|
||||||
|
private const val MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS = 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if the user has *at least 2* installed search engines.
|
||||||
|
* Useful to decide whether to show / enable certain functionalities.
|
||||||
|
*/
|
||||||
|
fun FenixSearchEngineProvider.areShortcutsAvailable(context: Context) =
|
||||||
|
installedSearchEngines(context).list.size >= MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS
|
@ -1,51 +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.session
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import mozilla.components.browser.state.selector.privateTabs
|
|
||||||
import mozilla.components.lib.state.ext.flowScoped
|
|
||||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This observer starts and stops the service to show a notification
|
|
||||||
* indicating that a private tab is open.
|
|
||||||
*/
|
|
||||||
class NotificationSessionObserver(
|
|
||||||
private val applicationContext: Context,
|
|
||||||
private val notificationService: SessionNotificationService.Companion = SessionNotificationService
|
|
||||||
) {
|
|
||||||
|
|
||||||
private var scope: CoroutineScope? = null
|
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
fun start() {
|
|
||||||
scope = applicationContext.components.core.store.flowScoped { flow ->
|
|
||||||
flow.map { state -> state.privateTabs.isNotEmpty() }
|
|
||||||
.ifChanged()
|
|
||||||
.collect { hasPrivateTabs ->
|
|
||||||
if (hasPrivateTabs) {
|
|
||||||
notificationService.start(applicationContext, isStartedFromPrivateShortcut)
|
|
||||||
} else if (SessionNotificationService.started) {
|
|
||||||
notificationService.stop(applicationContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
scope?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var isStartedFromPrivateShortcut = false
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,66 @@
|
|||||||
|
/* 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.session
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.metrics
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages notifications for private tabs.
|
||||||
|
*
|
||||||
|
* Private tab notifications solve two problems for us:
|
||||||
|
* 1 - They allow users to interact with us from outside of the app (example: by closing all
|
||||||
|
* private tabs).
|
||||||
|
* 2 - The notification will keep our process alive, allowing us to keep private tabs in memory.
|
||||||
|
*
|
||||||
|
* As long as a session is active this service will keep its notification alive.
|
||||||
|
*/
|
||||||
|
class PrivateNotificationService : AbstractPrivateNotificationService() {
|
||||||
|
|
||||||
|
override val store: BrowserStore by lazy { components.core.store }
|
||||||
|
|
||||||
|
override fun NotificationCompat.Builder.buildNotification() {
|
||||||
|
setSmallIcon(R.drawable.ic_pbm_notification)
|
||||||
|
setContentTitle(getString(R.string.app_name_private_4, getString(R.string.app_name)))
|
||||||
|
setContentText(getString(R.string.notification_pbm_delete_text_2))
|
||||||
|
color = ContextCompat.getColor(this@PrivateNotificationService, R.color.pbm_notification_color)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun erasePrivateTabs() {
|
||||||
|
metrics.track(Event.PrivateBrowsingNotificationTapped)
|
||||||
|
|
||||||
|
val homeScreenIntent = Intent(this, HomeActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
putExtra(HomeActivity.PRIVATE_BROWSING_MODE, isStartedFromPrivateShortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) {
|
||||||
|
// Set start mode to be in background (recents screen)
|
||||||
|
homeScreenIntent.apply {
|
||||||
|
putExtra(HomeActivity.START_IN_RECENTS_SCREEN, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivity(homeScreenIntent)
|
||||||
|
super.erasePrivateTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global used by [HomeActivity] to figure out if normal mode or private mode
|
||||||
|
* should be used after closing all private tabs.
|
||||||
|
*/
|
||||||
|
var isStartedFromPrivateShortcut = false
|
||||||
|
}
|
||||||
|
}
|
@ -1,174 +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.session
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import mozilla.components.browser.session.SessionManager
|
|
||||||
import mozilla.components.support.utils.ThreadUtils
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
import org.mozilla.fenix.ext.metrics
|
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages notifications for private tabs.
|
|
||||||
*
|
|
||||||
* Private tab notifications solve two problems for us:
|
|
||||||
* 1 - They allow users to interact with us from outside of the app (example: by closing all
|
|
||||||
* private tabs).
|
|
||||||
* 2 - The notification will keep our process alive, allowing us to keep private tabs in memory.
|
|
||||||
*
|
|
||||||
* As long as a session is active this service will keep its notification alive.
|
|
||||||
*/
|
|
||||||
class SessionNotificationService : Service() {
|
|
||||||
|
|
||||||
private var isStartedFromPrivateShortcut: Boolean = false
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
|
||||||
val action = intent.action ?: return START_NOT_STICKY
|
|
||||||
|
|
||||||
when (action) {
|
|
||||||
ACTION_START -> {
|
|
||||||
isStartedFromPrivateShortcut = intent.getBooleanExtra(STARTED_FROM_PRIVATE_SHORTCUT, false)
|
|
||||||
createNotificationChannelIfNeeded()
|
|
||||||
startForeground(NOTIFICATION_ID, buildNotification())
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTION_ERASE -> {
|
|
||||||
metrics.track(Event.PrivateBrowsingNotificationTapped)
|
|
||||||
|
|
||||||
val homeScreenIntent = Intent(this, HomeActivity::class.java)
|
|
||||||
val intentFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
homeScreenIntent.apply {
|
|
||||||
setFlags(intentFlags)
|
|
||||||
putExtra(HomeActivity.PRIVATE_BROWSING_MODE, isStartedFromPrivateShortcut)
|
|
||||||
}
|
|
||||||
if (VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) {
|
|
||||||
// Set start mode to be in background (recents screen)
|
|
||||||
homeScreenIntent.apply {
|
|
||||||
putExtra(HomeActivity.START_IN_RECENTS_SCREEN, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startActivity(homeScreenIntent)
|
|
||||||
components.core.sessionManager.removeAndCloseAllPrivateSessions()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> throw IllegalStateException("Unknown intent: $intent")
|
|
||||||
}
|
|
||||||
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent) {
|
|
||||||
components.core.sessionManager.removeAndCloseAllPrivateSessions()
|
|
||||||
|
|
||||||
stopForeground(true)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildNotification(): Notification {
|
|
||||||
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setSmallIcon(R.drawable.ic_pbm_notification)
|
|
||||||
.setContentTitle(getString(R.string.app_name_private_4, getString(R.string.app_name)))
|
|
||||||
.setContentText(getString(R.string.notification_pbm_delete_text_2))
|
|
||||||
.setContentIntent(createNotificationIntent())
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setLocalOnly(true)
|
|
||||||
.setColor(ContextCompat.getColor(this, R.color.pbm_notification_color))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNotificationIntent(): PendingIntent {
|
|
||||||
val intent = Intent(this, SessionNotificationService::class.java)
|
|
||||||
intent.action = ACTION_ERASE
|
|
||||||
|
|
||||||
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNotificationChannelIfNeeded() {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
// Notification channels are only available on Android O or higher.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val notificationManager = getSystemService<NotificationManager>() ?: return
|
|
||||||
|
|
||||||
val notificationChannelName = getString(R.string.notification_pbm_channel_name)
|
|
||||||
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
NOTIFICATION_CHANNEL_ID, notificationChannelName, NotificationManager.IMPORTANCE_MIN
|
|
||||||
)
|
|
||||||
channel.importance = NotificationManager.IMPORTANCE_LOW
|
|
||||||
channel.enableLights(false)
|
|
||||||
channel.enableVibration(false)
|
|
||||||
channel.setShowBadge(false)
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SessionManager.removeAndCloseAllPrivateSessions() {
|
|
||||||
sessionsOfType(private = true).forEach { remove(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val NOTIFICATION_ID = 83
|
|
||||||
private const val NOTIFICATION_CHANNEL_ID = "browsing-session"
|
|
||||||
private const val STARTED_FROM_PRIVATE_SHORTCUT = "STARTED_FROM_PRIVATE_SHORTCUT"
|
|
||||||
|
|
||||||
private const val ACTION_START = "start"
|
|
||||||
private const val ACTION_ERASE = "erase"
|
|
||||||
internal var started = false
|
|
||||||
|
|
||||||
internal fun start(
|
|
||||||
context: Context,
|
|
||||||
startedFromPrivateShortcut: Boolean
|
|
||||||
) {
|
|
||||||
val intent = Intent(context, SessionNotificationService::class.java)
|
|
||||||
intent.action = ACTION_START
|
|
||||||
intent.putExtra(STARTED_FROM_PRIVATE_SHORTCUT, startedFromPrivateShortcut)
|
|
||||||
|
|
||||||
// From Focus #2901: The application is crashing due to the service not calling `startForeground`
|
|
||||||
// before it times out. This is a speculative fix to decrease the time between these two
|
|
||||||
// calls by running this after potentially expensive calls in FocusApplication.onCreate and
|
|
||||||
// BrowserFragment.inflateView by posting it to the end of the main thread.
|
|
||||||
ThreadUtils.postToMainThread(Runnable {
|
|
||||||
context.startService(intent)
|
|
||||||
})
|
|
||||||
|
|
||||||
started = true
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun stop(context: Context) {
|
|
||||||
val intent = Intent(context, SessionNotificationService::class.java)
|
|
||||||
|
|
||||||
// We want to make sure we always call stop after start. So we're
|
|
||||||
// putting these actions on the same sequential run queue.
|
|
||||||
ThreadUtils.postToMainThread(Runnable {
|
|
||||||
context.stopService(intent)
|
|
||||||
})
|
|
||||||
|
|
||||||
started = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,120 @@
|
|||||||
|
/* 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.settings.about
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.util.Linkify
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the licenses of all the libraries used by Fenix.
|
||||||
|
*
|
||||||
|
* This is a re-implementation of play-services-oss-licenses library.
|
||||||
|
* We can't use the official implementation in the OSS flavor of Fenix
|
||||||
|
* because it is proprietary and closed-source.
|
||||||
|
*
|
||||||
|
* There are popular FLOSS alternatives to Google's plugin and library
|
||||||
|
* such as AboutLibraries (https://github.com/mikepenz/AboutLibraries)
|
||||||
|
* but we considered the risk of introducing such third-party dependency
|
||||||
|
* to Fenix too high. Therefore, we use Google's gradle plugin to
|
||||||
|
* extract the dependencies and their licenses, and this activity
|
||||||
|
* to show the extracted licenses to the end-user.
|
||||||
|
*/
|
||||||
|
class AboutLibrariesActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val appName = getString(R.string.app_name)
|
||||||
|
title = getString(R.string.open_source_licenses_title, appName)
|
||||||
|
setContentView(R.layout.about_libraries_activity)
|
||||||
|
|
||||||
|
setSupportActionBar(findViewById(R.id.toolbar))
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
setupLibrariesListView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLibrariesListView() {
|
||||||
|
val libraries = parseLibraries()
|
||||||
|
val listView = findViewById<ListView>(R.id.about_libraries_listview)
|
||||||
|
listView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, libraries)
|
||||||
|
listView.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
showLicenseDialog(libraries[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLibraries(): List<LibraryItem> {
|
||||||
|
/*
|
||||||
|
The gradle plugin "oss-licenses-plugin" creates two "raw" resources:
|
||||||
|
|
||||||
|
- third_party_licenses which is the binary concatenation of all the licenses text for
|
||||||
|
all the libraries. License texts can either be an URL to a license file or just the
|
||||||
|
raw text of the license.
|
||||||
|
|
||||||
|
- third_party_licenses_metadata which contains one dependency per line formatted in
|
||||||
|
the following way: "[start_offset]:[length] [name]"
|
||||||
|
|
||||||
|
[start_offset] : first byte in third_party_licenses that contains the license
|
||||||
|
text for this library.
|
||||||
|
[length] : length of the license text for this library in
|
||||||
|
third_party_licenses.
|
||||||
|
[name] : either the name of the library, or its artifact name.
|
||||||
|
|
||||||
|
See https://github.com/google/play-services-plugins/tree/master/oss-licenses-plugin
|
||||||
|
*/
|
||||||
|
val licensesData = resources
|
||||||
|
.openRawResource(R.raw.third_party_licenses)
|
||||||
|
.readBytes()
|
||||||
|
val licensesMetadataReader = resources
|
||||||
|
.openRawResource(R.raw.third_party_license_metadata)
|
||||||
|
.bufferedReader()
|
||||||
|
|
||||||
|
return licensesMetadataReader.use { reader -> reader.readLines() }.map { line ->
|
||||||
|
val (section, name) = line.split(" ", limit = 2)
|
||||||
|
val (startOffset, length) = section.split(":", limit = 2).map(String::toInt)
|
||||||
|
val licenseData = licensesData.sliceArray(startOffset until startOffset + length)
|
||||||
|
val licenseText = licenseData.toString(Charset.forName("UTF-8"))
|
||||||
|
LibraryItem(name, licenseText)
|
||||||
|
}.sortedBy { item -> item.name.toLowerCase(Locale.ROOT) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicenseDialog(libraryItem: LibraryItem) {
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setTitle(libraryItem.name)
|
||||||
|
.setMessage(libraryItem.license)
|
||||||
|
.create()
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
val textView = dialog.findViewById<TextView>(android.R.id.message)!!
|
||||||
|
Linkify.addLinks(textView, Linkify.ALL)
|
||||||
|
textView.linksClickable = true
|
||||||
|
textView.textSize = LICENSE_TEXT_SIZE
|
||||||
|
textView.typeface = Typeface.MONOSPACE
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LICENSE_TEXT_SIZE = 10F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LibraryItem(val name: String, val license: String) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/about_libraries"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
tools:context="org.mozilla.fenix.settings.about.AboutLibrariesActivity">
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.ActionBar" />
|
||||||
|
<ListView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/about_libraries_listview" />
|
||||||
|
</RelativeLayout>
|
@ -0,0 +1,98 @@
|
|||||||
|
/* 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.bookmarks
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.concept.menu.candidate.TextStyle
|
||||||
|
import mozilla.components.concept.storage.BookmarkNodeType
|
||||||
|
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu.Item
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class BookmarkItemMenuTest {
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var onItemTapped: (Item) -> Unit
|
||||||
|
private lateinit var menu: BookmarkItemMenu
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
|
||||||
|
onItemTapped = mockk(relaxed = true)
|
||||||
|
menu = BookmarkItemMenu(context, onItemTapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `delete item has special styling`() {
|
||||||
|
val deleteItem = menu.menuItems(BookmarkNodeType.SEPARATOR).last()
|
||||||
|
assertEquals("Delete", deleteItem.text)
|
||||||
|
assertEquals(
|
||||||
|
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
|
||||||
|
deleteItem.textStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
deleteItem.onClick()
|
||||||
|
verify { onItemTapped(Item.Delete) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `edit item appears for folders`() {
|
||||||
|
val folderItems = menu.menuItems(BookmarkNodeType.FOLDER)
|
||||||
|
assertEquals(2, folderItems.size)
|
||||||
|
val (edit, delete) = folderItems
|
||||||
|
|
||||||
|
assertEquals("Edit", edit.text)
|
||||||
|
edit.onClick()
|
||||||
|
verify { onItemTapped(Item.Edit) }
|
||||||
|
|
||||||
|
assertEquals("Delete", delete.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all item appears for sites`() {
|
||||||
|
val siteItems = menu.menuItems(BookmarkNodeType.ITEM)
|
||||||
|
assertEquals(6, siteItems.size)
|
||||||
|
val (edit, copy, share, openInNewTab, openInPrivateTab, delete) = siteItems
|
||||||
|
|
||||||
|
assertEquals("Edit", edit.text)
|
||||||
|
assertEquals("Copy", copy.text)
|
||||||
|
assertEquals("Share", share.text)
|
||||||
|
assertEquals("Open in new tab", openInNewTab.text)
|
||||||
|
assertEquals("Open in private tab", openInPrivateTab.text)
|
||||||
|
assertEquals("Delete", delete.text)
|
||||||
|
|
||||||
|
edit.onClick()
|
||||||
|
verify { onItemTapped(Item.Edit) }
|
||||||
|
|
||||||
|
copy.onClick()
|
||||||
|
verify { onItemTapped(Item.Copy) }
|
||||||
|
|
||||||
|
share.onClick()
|
||||||
|
verify { onItemTapped(Item.Share) }
|
||||||
|
|
||||||
|
openInNewTab.onClick()
|
||||||
|
verify { onItemTapped(Item.OpenInNewTab) }
|
||||||
|
|
||||||
|
openInPrivateTab.onClick()
|
||||||
|
verify { onItemTapped(Item.OpenInPrivateTab) }
|
||||||
|
|
||||||
|
delete.onClick()
|
||||||
|
verify { onItemTapped(Item.Delete) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun <T> List<T>.component6(): T {
|
||||||
|
return get(5)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/* 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.bookmarks.viewholders
|
||||||
|
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
|
import mozilla.components.concept.storage.BookmarkNodeType
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.ext.hideAndDisable
|
||||||
|
import org.mozilla.fenix.ext.showAndEnable
|
||||||
|
import org.mozilla.fenix.library.LibrarySiteItemView
|
||||||
|
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentInteractor
|
||||||
|
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
|
||||||
|
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
|
||||||
|
|
||||||
|
class BookmarkFolderViewHolderTest {
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
private lateinit var interactor: BookmarkFragmentInteractor
|
||||||
|
@MockK(relaxed = true)
|
||||||
|
private lateinit var siteItemView: LibrarySiteItemView
|
||||||
|
private lateinit var holder: BookmarkFolderViewHolder
|
||||||
|
|
||||||
|
private val folder = BookmarkNode(
|
||||||
|
type = BookmarkNodeType.FOLDER,
|
||||||
|
guid = "456",
|
||||||
|
parentGuid = "123",
|
||||||
|
position = 0,
|
||||||
|
title = "Folder",
|
||||||
|
url = null,
|
||||||
|
children = listOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
|
||||||
|
mockkStatic(AppCompatResources::class)
|
||||||
|
every { AppCompatResources.getDrawable(any(), any()) } returns mockk(relaxed = true)
|
||||||
|
|
||||||
|
holder = BookmarkFolderViewHolder(siteItemView, interactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `binds title and selected state`() {
|
||||||
|
holder.bind(folder, BookmarkFragmentState.Mode.Normal())
|
||||||
|
|
||||||
|
verify {
|
||||||
|
siteItemView.titleView.text = folder.title
|
||||||
|
siteItemView.overflowView.showAndEnable()
|
||||||
|
siteItemView.changeSelected(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.bind(folder, BookmarkFragmentState.Mode.Selecting(setOf(folder)))
|
||||||
|
|
||||||
|
verify {
|
||||||
|
siteItemView.titleView.text = folder.title
|
||||||
|
siteItemView.overflowView.hideAndDisable()
|
||||||
|
siteItemView.changeSelected(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind with payload of no changes does not rebind views`() {
|
||||||
|
holder.bind(
|
||||||
|
folder,
|
||||||
|
BookmarkFragmentState.Mode.Normal(),
|
||||||
|
BookmarkPayload(false, false, false, false)
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(inverse = true) {
|
||||||
|
siteItemView.titleView.text = folder.title
|
||||||
|
siteItemView.overflowView.showAndEnable()
|
||||||
|
siteItemView.overflowView.hideAndDisable()
|
||||||
|
siteItemView.changeSelected(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
/* 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.bookmarks.viewholders
|
||||||
|
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
|
import mozilla.components.concept.storage.BookmarkNodeType
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.ext.hideAndDisable
|
||||||
|
import org.mozilla.fenix.ext.showAndEnable
|
||||||
|
import org.mozilla.fenix.library.LibrarySiteItemView
|
||||||
|
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentInteractor
|
||||||
|
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
|
||||||
|
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
|
||||||
|
|
||||||
|
class BookmarkItemViewHolderTest {
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
private lateinit var interactor: BookmarkFragmentInteractor
|
||||||
|
|
||||||
|
@MockK(relaxed = true)
|
||||||
|
private lateinit var siteItemView: LibrarySiteItemView
|
||||||
|
|
||||||
|
private lateinit var holder: BookmarkItemViewHolder
|
||||||
|
|
||||||
|
private val item = BookmarkNode(
|
||||||
|
type = BookmarkNodeType.ITEM,
|
||||||
|
guid = "456",
|
||||||
|
parentGuid = "123",
|
||||||
|
position = 0,
|
||||||
|
title = "Mozilla",
|
||||||
|
url = "https://www.mozilla.org",
|
||||||
|
children = listOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
holder = BookmarkItemViewHolder(siteItemView, interactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `binds views for unselected item`() {
|
||||||
|
val mode = BookmarkFragmentState.Mode.Normal()
|
||||||
|
holder.bind(item, mode)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
siteItemView.setSelectionInteractor(item, mode, interactor)
|
||||||
|
siteItemView.titleView.text = item.title
|
||||||
|
siteItemView.urlView.text = item.url
|
||||||
|
siteItemView.overflowView.showAndEnable()
|
||||||
|
siteItemView.changeSelected(false)
|
||||||
|
holder.setColorsAndIcons(item.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `binds views for selected item`() {
|
||||||
|
val mode = BookmarkFragmentState.Mode.Selecting(setOf(item))
|
||||||
|
holder.bind(item, mode)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
siteItemView.setSelectionInteractor(item, mode, interactor)
|
||||||
|
siteItemView.titleView.text = item.title
|
||||||
|
siteItemView.urlView.text = item.url
|
||||||
|
siteItemView.overflowView.hideAndDisable()
|
||||||
|
siteItemView.changeSelected(true)
|
||||||
|
holder.setColorsAndIcons(item.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind with payload of no changes does not rebind views`() {
|
||||||
|
holder.bind(
|
||||||
|
item,
|
||||||
|
BookmarkFragmentState.Mode.Normal(),
|
||||||
|
BookmarkPayload(false, false, false, false)
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(inverse = true) {
|
||||||
|
siteItemView.titleView.text = item.title
|
||||||
|
siteItemView.urlView.text = item.url
|
||||||
|
siteItemView.overflowView.showAndEnable()
|
||||||
|
siteItemView.overflowView.hideAndDisable()
|
||||||
|
siteItemView.changeSelected(any())
|
||||||
|
holder.setColorsAndIcons(item.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `binding an item with a null title uses the url as the title`() {
|
||||||
|
val item = item.copy(title = null)
|
||||||
|
holder.bind(item, BookmarkFragmentState.Mode.Normal())
|
||||||
|
|
||||||
|
verify { siteItemView.titleView.text = item.url }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `binding an item with a blank title uses the url as the title`() {
|
||||||
|
val item = item.copy(title = " ")
|
||||||
|
holder.bind(item, BookmarkFragmentState.Mode.Normal())
|
||||||
|
|
||||||
|
verify { siteItemView.titleView.text = item.url }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `rebinds title if item title is null and the item url has changed`() {
|
||||||
|
val item = item.copy(title = null)
|
||||||
|
holder.bind(
|
||||||
|
item,
|
||||||
|
BookmarkFragmentState.Mode.Normal(),
|
||||||
|
BookmarkPayload(
|
||||||
|
titleChanged = false,
|
||||||
|
urlChanged = true,
|
||||||
|
selectedChanged = false,
|
||||||
|
modeChanged = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
verify { siteItemView.titleView.text = item.url }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `rebinds title if item title is blank and the item url has changed`() {
|
||||||
|
val item = item.copy(title = " ")
|
||||||
|
holder.bind(
|
||||||
|
item,
|
||||||
|
BookmarkFragmentState.Mode.Normal(),
|
||||||
|
BookmarkPayload(
|
||||||
|
titleChanged = false,
|
||||||
|
urlChanged = true,
|
||||||
|
selectedChanged = false,
|
||||||
|
modeChanged = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
verify { siteItemView.titleView.text = item.url }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
/* 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.history
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.concept.menu.candidate.TextStyle
|
||||||
|
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
import org.mozilla.fenix.library.history.HistoryItemMenu.Item
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class HistoryItemMenuTest {
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var onItemTapped: (Item) -> Unit
|
||||||
|
private lateinit var menu: HistoryItemMenu
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
|
||||||
|
onItemTapped = mockk(relaxed = true)
|
||||||
|
menu = HistoryItemMenu(context, onItemTapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `delete item has special styling`() {
|
||||||
|
val deleteItem = menu.menuItems().last()
|
||||||
|
assertEquals("Delete", deleteItem.text)
|
||||||
|
assertEquals(
|
||||||
|
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
|
||||||
|
deleteItem.textStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
deleteItem.onClick()
|
||||||
|
verify { onItemTapped(Item.Delete) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `builds menu items`() {
|
||||||
|
val items = menu.menuItems()
|
||||||
|
assertEquals(5, items.size)
|
||||||
|
val (copy, share, openInNewTab, openInPrivateTab, delete) = items
|
||||||
|
|
||||||
|
assertEquals("Copy", copy.text)
|
||||||
|
assertEquals("Share", share.text)
|
||||||
|
assertEquals("Open in new tab", openInNewTab.text)
|
||||||
|
assertEquals("Open in private tab", openInPrivateTab.text)
|
||||||
|
assertEquals("Delete", delete.text)
|
||||||
|
|
||||||
|
copy.onClick()
|
||||||
|
verify { onItemTapped(Item.Copy) }
|
||||||
|
|
||||||
|
share.onClick()
|
||||||
|
verify { onItemTapped(Item.Share) }
|
||||||
|
|
||||||
|
openInNewTab.onClick()
|
||||||
|
verify { onItemTapped(Item.OpenInNewTab) }
|
||||||
|
|
||||||
|
openInPrivateTab.onClick()
|
||||||
|
verify { onItemTapped(Item.OpenInPrivateTab) }
|
||||||
|
|
||||||
|
delete.onClick()
|
||||||
|
verify { onItemTapped(Item.Delete) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,320 @@
|
|||||||
|
/* 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.searchdialog
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDirections
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.unmockkObject
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import mozilla.components.browser.search.SearchEngine
|
||||||
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricsUtils
|
||||||
|
import org.mozilla.fenix.ext.navigateSafe
|
||||||
|
import org.mozilla.fenix.search.SearchFragmentAction
|
||||||
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class SearchDialogControllerTest {
|
||||||
|
|
||||||
|
@MockK(relaxed = true) private lateinit var activity: HomeActivity
|
||||||
|
@MockK(relaxed = true) private lateinit var store: SearchDialogFragmentStore
|
||||||
|
@MockK(relaxed = true) private lateinit var navController: NavController
|
||||||
|
@MockK private lateinit var searchEngine: SearchEngine
|
||||||
|
@MockK(relaxed = true) private lateinit var metrics: MetricController
|
||||||
|
@MockK(relaxed = true) private lateinit var settings: Settings
|
||||||
|
@MockK private lateinit var sessionManager: SessionManager
|
||||||
|
@MockK(relaxed = true) private lateinit var clearToolbarFocus: () -> Unit
|
||||||
|
|
||||||
|
private lateinit var controller: SearchDialogController
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
mockkObject(MetricsUtils)
|
||||||
|
|
||||||
|
every { store.state.tabId } returns "test-tab-id"
|
||||||
|
every { store.state.searchEngineSource.searchEngine } returns searchEngine
|
||||||
|
every { sessionManager.select(any()) } just Runs
|
||||||
|
every { MetricsUtils.createSearchEvent(searchEngine, activity, any()) } returns null
|
||||||
|
|
||||||
|
controller = SearchDialogController(
|
||||||
|
activity = activity,
|
||||||
|
sessionManager = sessionManager,
|
||||||
|
store = store,
|
||||||
|
navController = navController,
|
||||||
|
settings = settings,
|
||||||
|
metrics = metrics,
|
||||||
|
clearToolbarFocus = clearToolbarFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun teardown() {
|
||||||
|
unmockkObject(MetricsUtils)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleUrlCommitted() {
|
||||||
|
val url = "https://www.google.com/"
|
||||||
|
|
||||||
|
controller.handleUrlCommitted(url)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
activity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = false,
|
||||||
|
from = BrowserDirection.FromSearchDialog,
|
||||||
|
engine = searchEngine
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify { metrics.track(Event.EnteredUrl(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSearchCommitted() {
|
||||||
|
val searchTerm = "Firefox"
|
||||||
|
|
||||||
|
controller.handleUrlCommitted(searchTerm)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
activity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = searchTerm,
|
||||||
|
newTab = false,
|
||||||
|
from = BrowserDirection.FromSearchDialog,
|
||||||
|
engine = searchEngine
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify { settings.incrementActiveSearchCount() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCrashesUrlCommitted() {
|
||||||
|
val url = "about:crashes"
|
||||||
|
every { activity.packageName } returns "org.mozilla.fenix"
|
||||||
|
|
||||||
|
controller.handleUrlCommitted(url)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
activity.startActivity(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleMozillaUrlCommitted() {
|
||||||
|
val url = "moz://a"
|
||||||
|
|
||||||
|
controller.handleUrlCommitted(url)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
activity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO),
|
||||||
|
newTab = false,
|
||||||
|
from = BrowserDirection.FromSearchDialog,
|
||||||
|
engine = searchEngine
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify { metrics.track(Event.EnteredUrl(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleEditingCancelled() = runBlockingTest {
|
||||||
|
controller.handleEditingCancelled()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
clearToolbarFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleTextChangedNonEmpty() {
|
||||||
|
val text = "fenix"
|
||||||
|
|
||||||
|
controller.handleTextChanged(text)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleTextChangedEmpty() {
|
||||||
|
val text = ""
|
||||||
|
|
||||||
|
controller.handleTextChanged(text)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `show search shortcuts when setting enabled AND query empty`() {
|
||||||
|
val text = ""
|
||||||
|
every { settings.shouldShowSearchShortcuts } returns true
|
||||||
|
|
||||||
|
controller.handleTextChanged(text)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `show search shortcuts when setting enabled AND query equals url`() {
|
||||||
|
val text = "mozilla.org"
|
||||||
|
every { store.state.url } returns "mozilla.org"
|
||||||
|
every { settings.shouldShowSearchShortcuts } returns true
|
||||||
|
|
||||||
|
controller.handleTextChanged(text)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `do not show search shortcuts when setting enabled AND query non-empty`() {
|
||||||
|
val text = "mozilla"
|
||||||
|
|
||||||
|
controller.handleTextChanged(text)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `do not show search shortcuts when setting disabled AND query empty AND url not matching query`() {
|
||||||
|
every { settings.shouldShowSearchShortcuts } returns false
|
||||||
|
|
||||||
|
val text = ""
|
||||||
|
|
||||||
|
controller.handleTextChanged(text)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `do not show search shortcuts when setting disabled AND query non-empty`() {
|
||||||
|
every { settings.shouldShowSearchShortcuts } returns false
|
||||||
|
|
||||||
|
val text = "mozilla"
|
||||||
|
|
||||||
|
controller.handleTextChanged(text)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleUrlTapped() {
|
||||||
|
val url = "https://www.google.com/"
|
||||||
|
|
||||||
|
controller.handleUrlTapped(url)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
activity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = false,
|
||||||
|
from = BrowserDirection.FromSearchDialog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify { metrics.track(Event.EnteredUrl(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSearchTermsTapped() {
|
||||||
|
val searchTerms = "fenix"
|
||||||
|
|
||||||
|
controller.handleSearchTermsTapped(searchTerms)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
activity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = searchTerms,
|
||||||
|
newTab = false,
|
||||||
|
from = BrowserDirection.FromSearchDialog,
|
||||||
|
engine = searchEngine,
|
||||||
|
forceSearch = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSearchShortcutEngineSelected() {
|
||||||
|
val searchEngine: SearchEngine = mockk(relaxed = true)
|
||||||
|
|
||||||
|
controller.handleSearchShortcutEngineSelected(searchEngine)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine)) }
|
||||||
|
verify { metrics.track(Event.SearchShortcutSelected(searchEngine, false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleClickSearchEngineSettings() {
|
||||||
|
val directions: NavDirections =
|
||||||
|
SearchDialogFragmentDirections.actionGlobalSearchEngineFragment()
|
||||||
|
|
||||||
|
controller.handleClickSearchEngineSettings()
|
||||||
|
|
||||||
|
verify { navController.navigateSafe(R.id.searchEngineFragment, directions) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSearchShortcutsButtonClicked_alreadyOpen() {
|
||||||
|
every { store.state.showSearchShortcuts } returns true
|
||||||
|
|
||||||
|
controller.handleSearchShortcutsButtonClicked()
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSearchShortcutsButtonClicked_notYetOpen() {
|
||||||
|
every { store.state.showSearchShortcuts } returns false
|
||||||
|
|
||||||
|
controller.handleSearchShortcutsButtonClicked()
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleExistingSessionSelected() {
|
||||||
|
val session = mockk<Session>()
|
||||||
|
|
||||||
|
controller.handleExistingSessionSelected(session)
|
||||||
|
|
||||||
|
verify { sessionManager.select(session) }
|
||||||
|
verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleExistingSessionSelected_tabId_nullSession() {
|
||||||
|
every { sessionManager.findSessionById("tab-id") } returns null
|
||||||
|
|
||||||
|
controller.handleExistingSessionSelected("tab-id")
|
||||||
|
|
||||||
|
verify(inverse = true) { sessionManager.select(any()) }
|
||||||
|
verify(inverse = true) { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleExistingSessionSelected_tabId() {
|
||||||
|
val session = mockk<Session>()
|
||||||
|
every { sessionManager.findSessionById("tab-id") } returns session
|
||||||
|
|
||||||
|
controller.handleExistingSessionSelected("tab-id")
|
||||||
|
|
||||||
|
verify { sessionManager.select(any()) }
|
||||||
|
verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +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.session
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import io.mockk.Called
|
|
||||||
import io.mockk.MockKAnnotations
|
|
||||||
import io.mockk.confirmVerified
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.impl.annotations.MockK
|
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import mozilla.components.browser.state.action.CustomTabListAction
|
|
||||||
import mozilla.components.browser.state.action.TabListAction
|
|
||||||
import mozilla.components.browser.state.state.createCustomTab
|
|
||||||
import mozilla.components.browser.state.state.createTab
|
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
@RunWith(FenixRobolectricTestRunner::class)
|
|
||||||
class NotificationSessionObserverTest {
|
|
||||||
|
|
||||||
private lateinit var observer: NotificationSessionObserver
|
|
||||||
private lateinit var store: BrowserStore
|
|
||||||
@MockK private lateinit var context: Context
|
|
||||||
@MockK(relaxed = true) private lateinit var notificationService: SessionNotificationService.Companion
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun before() {
|
|
||||||
MockKAnnotations.init(this)
|
|
||||||
store = BrowserStore()
|
|
||||||
every { context.components.core.store } returns store
|
|
||||||
observer = NotificationSessionObserver(context, notificationService)
|
|
||||||
NotificationSessionObserver.isStartedFromPrivateShortcut = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN session is private and non-custom WHEN it is added THEN notification service should be started`() = runBlocking {
|
|
||||||
val privateSession = createTab("https://firefox.com", private = true)
|
|
||||||
|
|
||||||
store.dispatch(TabListAction.AddTabAction(privateSession)).join()
|
|
||||||
|
|
||||||
observer.start()
|
|
||||||
verify(exactly = 1) { notificationService.start(context, false) }
|
|
||||||
confirmVerified(notificationService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN session is not private WHEN it is added THEN notification service should not be started`() = runBlocking {
|
|
||||||
val normalSession = createTab("https://firefox.com")
|
|
||||||
val customSession = createCustomTab("https://firefox.com")
|
|
||||||
|
|
||||||
observer.start()
|
|
||||||
verify { notificationService wasNot Called }
|
|
||||||
|
|
||||||
store.dispatch(TabListAction.AddTabAction(normalSession)).join()
|
|
||||||
verify(exactly = 0) { notificationService.start(context, false) }
|
|
||||||
|
|
||||||
store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
|
|
||||||
verify(exactly = 0) { notificationService.start(context, false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN session is custom tab WHEN it is added THEN notification service should not be started`() = runBlocking {
|
|
||||||
val privateCustomSession = createCustomTab("https://firefox.com").let {
|
|
||||||
it.copy(content = it.content.copy(private = true))
|
|
||||||
}
|
|
||||||
val customSession = createCustomTab("https://firefox.com")
|
|
||||||
|
|
||||||
observer.start()
|
|
||||||
verify { notificationService wasNot Called }
|
|
||||||
|
|
||||||
store.dispatch(CustomTabListAction.AddCustomTabAction(privateCustomSession)).join()
|
|
||||||
verify(exactly = 0) { notificationService.start(context, false) }
|
|
||||||
|
|
||||||
store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
|
|
||||||
verify(exactly = 0) { notificationService.start(context, false) }
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,64 @@
|
|||||||
|
/* 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.session
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService.Companion.ACTION_ERASE
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
import org.robolectric.Robolectric
|
||||||
|
import org.robolectric.Shadows.shadowOf
|
||||||
|
import org.robolectric.android.controller.ServiceController
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class PrivateNotificationServiceTest {
|
||||||
|
|
||||||
|
private lateinit var controller: ServiceController<PrivateNotificationService>
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
val store = testContext.components.core.store
|
||||||
|
every { store.dispatch(any()) } returns mockk()
|
||||||
|
|
||||||
|
controller = Robolectric.buildService(
|
||||||
|
PrivateNotificationService::class.java,
|
||||||
|
Intent(ACTION_ERASE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `service opens home activity with PBM flag set to true`() {
|
||||||
|
PrivateNotificationService.isStartedFromPrivateShortcut = true
|
||||||
|
val service = shadowOf(controller.get())
|
||||||
|
controller.startCommand(0, 0)
|
||||||
|
|
||||||
|
val intent = service.nextStartedActivity
|
||||||
|
assertEquals(ComponentName(testContext, HomeActivity::class.java), intent.component)
|
||||||
|
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK, intent.flags)
|
||||||
|
assertEquals(true, intent.extras?.getBoolean(PRIVATE_BROWSING_MODE))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `service opens home activity with PBM flag set to false`() {
|
||||||
|
PrivateNotificationService.isStartedFromPrivateShortcut = false
|
||||||
|
val service = shadowOf(controller.get())
|
||||||
|
controller.startCommand(0, 0)
|
||||||
|
|
||||||
|
val intent = service.nextStartedActivity
|
||||||
|
assertEquals(ComponentName(testContext, HomeActivity::class.java), intent.component)
|
||||||
|
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK, intent.flags)
|
||||||
|
assertEquals(false, intent.extras?.getBoolean(PRIVATE_BROWSING_MODE))
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +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.session
|
|
||||||
|
|
||||||
import mozilla.components.support.test.robolectric.testContext
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
|
||||||
|
|
||||||
@RunWith(FenixRobolectricTestRunner::class)
|
|
||||||
class SessionNotificationServiceTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Service keeps tracked of started state`() {
|
|
||||||
assertFalse(SessionNotificationService.started)
|
|
||||||
|
|
||||||
SessionNotificationService.start(testContext, false)
|
|
||||||
assertTrue(SessionNotificationService.started)
|
|
||||||
|
|
||||||
SessionNotificationService.stop(testContext)
|
|
||||||
assertFalse(SessionNotificationService.started)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,45 @@
|
|||||||
|
/* 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.settings.about
|
||||||
|
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.TextView
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
import org.robolectric.Robolectric
|
||||||
|
import org.robolectric.Shadows.shadowOf
|
||||||
|
import org.robolectric.shadows.ShadowAlertDialog
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class AboutLibrariesActivityTest {
|
||||||
|
@Test
|
||||||
|
fun `activity should display licenses`() {
|
||||||
|
val activity = Robolectric.buildActivity(AboutLibrariesActivity::class.java).create().get()
|
||||||
|
val listView = activity.findViewById<ListView>(R.id.about_libraries_listview)
|
||||||
|
|
||||||
|
assertTrue(0 < listView.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `item click should open license dialog`() {
|
||||||
|
val activity = Robolectric.buildActivity(AboutLibrariesActivity::class.java).create().get()
|
||||||
|
|
||||||
|
val listView = activity.findViewById<ListView>(R.id.about_libraries_listview)
|
||||||
|
val listViewShadow = shadowOf(listView)
|
||||||
|
listViewShadow.clickFirstItemContainingText("org.mozilla.geckoview:geckoview")
|
||||||
|
|
||||||
|
val alertDialogShadow = ShadowAlertDialog.getLatestDialog()
|
||||||
|
assertTrue(alertDialogShadow.isShowing)
|
||||||
|
|
||||||
|
val alertDialogText = alertDialogShadow
|
||||||
|
.findViewById<TextView>(android.R.id.message)
|
||||||
|
.text
|
||||||
|
.toString()
|
||||||
|
assertTrue(alertDialogText.contains("MPL"))
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue