-
-
+
diff --git a/app/src/main/java/acr/browser/lightning/BrowserApp.kt b/app/src/main/java/acr/browser/lightning/BrowserApp.kt
index c979c61ff..8d6c4e74a 100644
--- a/app/src/main/java/acr/browser/lightning/BrowserApp.kt
+++ b/app/src/main/java/acr/browser/lightning/BrowserApp.kt
@@ -1,25 +1,23 @@
package acr.browser.lightning
+import acr.browser.lightning.browser.di.AppComponent
+import acr.browser.lightning.browser.di.DaggerAppComponent
+import acr.browser.lightning.browser.di.DatabaseScheduler
+import acr.browser.lightning.browser.di.injector
+import acr.browser.lightning.browser.proxy.ProxyAdapter
import acr.browser.lightning.database.bookmark.BookmarkExporter
import acr.browser.lightning.database.bookmark.BookmarkRepository
import acr.browser.lightning.device.BuildInfo
import acr.browser.lightning.device.BuildType
-import acr.browser.lightning.di.AppComponent
-import acr.browser.lightning.di.DaggerAppComponent
-import acr.browser.lightning.di.DatabaseScheduler
-import acr.browser.lightning.di.injector
import acr.browser.lightning.log.Logger
import acr.browser.lightning.preference.DeveloperPreferences
import acr.browser.lightning.utils.FileUtils
import acr.browser.lightning.utils.MemoryLeakUtils
-import acr.browser.lightning.utils.installMultiDex
import android.app.Activity
import android.app.Application
-import android.content.Context
import android.os.Build
import android.os.StrictMode
import android.webkit.WebView
-import androidx.appcompat.app.AppCompatDelegate
import com.squareup.leakcanary.LeakCanary
import io.reactivex.Scheduler
import io.reactivex.Single
@@ -27,34 +25,47 @@ import io.reactivex.plugins.RxJavaPlugins
import javax.inject.Inject
import kotlin.system.exitProcess
+/**
+ * The browser application.
+ */
class BrowserApp : Application() {
- @Inject internal lateinit var developerPreferences: DeveloperPreferences
- @Inject internal lateinit var bookmarkModel: BookmarkRepository
- @Inject @field:DatabaseScheduler internal lateinit var databaseScheduler: Scheduler
- @Inject internal lateinit var logger: Logger
- @Inject internal lateinit var buildInfo: BuildInfo
+ @Inject
+ internal lateinit var developerPreferences: DeveloperPreferences
- lateinit var applicationComponent: AppComponent
+ @Inject
+ internal lateinit var bookmarkModel: BookmarkRepository
- override fun attachBaseContext(base: Context) {
- super.attachBaseContext(base)
- if (BuildConfig.DEBUG && Build.VERSION.SDK_INT < 21) {
- installMultiDex(context = base)
- }
- }
+ @Inject
+ @field:DatabaseScheduler
+ internal lateinit var databaseScheduler: Scheduler
+
+ @Inject
+ internal lateinit var logger: Logger
+
+ @Inject
+ internal lateinit var buildInfo: BuildInfo
+
+ @Inject
+ internal lateinit var proxyAdapter: ProxyAdapter
+
+ lateinit var applicationComponent: AppComponent
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
- StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
- .detectAll()
- .penaltyLog()
- .build())
- StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
- .detectAll()
- .penaltyLog()
- .build())
+ StrictMode.setThreadPolicy(
+ StrictMode.ThreadPolicy.Builder()
+ .detectAll()
+ .penaltyLog()
+ .build()
+ )
+ StrictMode.setVmPolicy(
+ StrictMode.VmPolicy.Builder()
+ .detectAll()
+ .penaltyLog()
+ .build()
+ )
}
if (Build.VERSION.SDK_INT >= 28) {
@@ -112,22 +123,21 @@ class BrowserApp : Application() {
MemoryLeakUtils.clearNextServedView(activity, this@BrowserApp)
}
})
+
+ registerActivityLifecycleCallbacks(proxyAdapter)
}
/**
* Create the [BuildType] from the [BuildConfig].
*/
- private fun createBuildInfo() = BuildInfo(when {
- BuildConfig.DEBUG -> BuildType.DEBUG
- else -> BuildType.RELEASE
- })
+ private fun createBuildInfo() = BuildInfo(
+ when {
+ BuildConfig.DEBUG -> BuildType.DEBUG
+ else -> BuildType.RELEASE
+ }
+ )
companion object {
private const val TAG = "BrowserApp"
-
- init {
- AppCompatDelegate.setCompatVectorFromResourcesEnabled(Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT)
- }
}
-
}
diff --git a/app/src/main/java/acr/browser/lightning/Capabilities.kt b/app/src/main/java/acr/browser/lightning/Capabilities.kt
index 5099f16be..201b7e6a5 100644
--- a/app/src/main/java/acr/browser/lightning/Capabilities.kt
+++ b/app/src/main/java/acr/browser/lightning/Capabilities.kt
@@ -6,9 +6,7 @@ import android.os.Build
* Capabilities that are specific to certain API levels.
*/
enum class Capabilities {
- FULL_INCOGNITO,
- WEB_RTC,
- THIRD_PARTY_COOKIE_BLOCKING
+ FULL_INCOGNITO
}
/**
@@ -17,6 +15,4 @@ enum class Capabilities {
val Capabilities.isSupported: Boolean
get() = when (this) {
Capabilities.FULL_INCOGNITO -> Build.VERSION.SDK_INT >= 28
- Capabilities.WEB_RTC -> Build.VERSION.SDK_INT >= 21
- Capabilities.THIRD_PARTY_COOKIE_BLOCKING -> Build.VERSION.SDK_INT >= 21
}
diff --git a/app/src/main/java/acr/browser/lightning/DefaultBrowserActivity.kt b/app/src/main/java/acr/browser/lightning/DefaultBrowserActivity.kt
new file mode 100644
index 000000000..dde5a7b0f
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/DefaultBrowserActivity.kt
@@ -0,0 +1,14 @@
+package acr.browser.lightning
+
+import acr.browser.lightning.browser.BrowserActivity
+
+/**
+ * The default browsing experience.
+ */
+class DefaultBrowserActivity : BrowserActivity() {
+ override fun isIncognito(): Boolean = false
+
+ override fun menu(): Int = R.menu.main
+
+ override fun homeIcon(): Int = R.drawable.ic_action_home
+}
diff --git a/app/src/main/java/acr/browser/lightning/IncognitoActivity.kt b/app/src/main/java/acr/browser/lightning/IncognitoActivity.kt
deleted file mode 100644
index bf1eebb6c..000000000
--- a/app/src/main/java/acr/browser/lightning/IncognitoActivity.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package acr.browser.lightning
-
-import acr.browser.lightning.browser.activity.BrowserActivity
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.view.Menu
-import android.webkit.CookieManager
-import android.webkit.CookieSyncManager
-import io.reactivex.Completable
-
-class IncognitoActivity : BrowserActivity() {
-
- override fun provideThemeOverride(): Int? = R.style.Theme_DarkTheme
-
- @Suppress("DEPRECATION")
- public override fun updateCookiePreference(): Completable = Completable.fromAction {
- val cookieManager = CookieManager.getInstance()
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
- CookieSyncManager.createInstance(this@IncognitoActivity)
- }
- if (Capabilities.FULL_INCOGNITO.isSupported) {
- cookieManager.setAcceptCookie(userPreferences.cookiesEnabled)
- } else {
- cookieManager.setAcceptCookie(userPreferences.incognitoCookiesEnabled)
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.incognito, menu)
- return super.onCreateOptionsMenu(menu)
- }
-
- @Suppress("RedundantOverride")
- override fun onNewIntent(intent: Intent) {
- handleNewIntent(intent)
- super.onNewIntent(intent)
- }
-
- @Suppress("RedundantOverride")
- override fun onPause() = super.onPause() // saveOpenTabs();
-
- override fun updateHistory(title: String?, url: String) = Unit // addItemToHistory(title, url)
-
- override fun isIncognito() = true
-
- override fun closeActivity() = closeDrawers(::closeBrowser)
-
- companion object {
- /**
- * Creates the intent with which to launch the activity. Adds the reorder to front flag.
- */
- fun createIntent(context: Context, uri: Uri? = null) = Intent(context, IncognitoActivity::class.java).apply {
- flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
- data = uri
- }
- }
-}
diff --git a/app/src/main/java/acr/browser/lightning/IncognitoBrowserActivity.kt b/app/src/main/java/acr/browser/lightning/IncognitoBrowserActivity.kt
new file mode 100644
index 000000000..4b509dbd5
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/IncognitoBrowserActivity.kt
@@ -0,0 +1,37 @@
+package acr.browser.lightning
+
+import acr.browser.lightning.browser.BrowserActivity
+import android.app.Activity
+import android.content.Intent
+import androidx.core.net.toUri
+
+/**
+ * The incognito browsing experience.
+ */
+class IncognitoBrowserActivity : BrowserActivity() {
+
+ override fun provideThemeOverride(): Int = R.style.Theme_DarkTheme
+
+ override fun isIncognito(): Boolean = true
+
+ override fun menu(): Int = R.menu.incognito
+
+ override fun homeIcon(): Int = R.drawable.incognito_mode
+
+ companion object {
+ /**
+ * Creates an intent to launch the browser with an optional [url] to load.
+ */
+ fun intent(activity: Activity, url: String? = null): Intent =
+ Intent(activity, IncognitoBrowserActivity::class.java).apply {
+ data = url?.let(String::toUri)
+ }
+
+ /**
+ * Launch the browser with an optional [url] to load.
+ */
+ fun launch(activity: Activity, url: String?) {
+ activity.startActivity(intent(activity, url))
+ }
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/MainActivity.kt b/app/src/main/java/acr/browser/lightning/MainActivity.kt
deleted file mode 100644
index 98a55b316..000000000
--- a/app/src/main/java/acr/browser/lightning/MainActivity.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package acr.browser.lightning
-
-import acr.browser.lightning.browser.activity.BrowserActivity
-import android.content.Intent
-import android.os.Build
-import android.view.KeyEvent
-import android.view.Menu
-import android.webkit.CookieManager
-import android.webkit.CookieSyncManager
-import io.reactivex.Completable
-
-class MainActivity : BrowserActivity() {
-
- @Suppress("DEPRECATION")
- public override fun updateCookiePreference(): Completable = Completable.fromAction {
- val cookieManager = CookieManager.getInstance()
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
- CookieSyncManager.createInstance(this@MainActivity)
- }
- cookieManager.setAcceptCookie(userPreferences.cookiesEnabled)
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.main, menu)
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onNewIntent(intent: Intent) =
- if (intent.action == INTENT_PANIC_TRIGGER) {
- panicClean()
- } else {
- handleNewIntent(intent)
- super.onNewIntent(intent)
- }
-
- override fun onPause() {
- super.onPause()
- saveOpenTabs()
- }
-
- override fun updateHistory(title: String?, url: String) = addItemToHistory(title, url)
-
- override fun isIncognito() = false
-
- override fun closeActivity() = closeDrawers {
- performExitCleanUp()
- moveTaskToBack(true)
- }
-
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- if (event.action == KeyEvent.ACTION_DOWN && event.isCtrlPressed) {
- when (event.keyCode) {
- KeyEvent.KEYCODE_P ->
- // Open a new private window
- if (event.isShiftPressed) {
- startActivity(IncognitoActivity.createIntent(this))
- overridePendingTransition(R.anim.slide_up_in, R.anim.fade_out_scale)
- return true
- }
- }
- }
- return super.dispatchKeyEvent(event)
- }
-
-
-}
diff --git a/app/src/main/java/acr/browser/lightning/browser/activity/ThemableBrowserActivity.kt b/app/src/main/java/acr/browser/lightning/ThemableBrowserActivity.kt
similarity index 72%
rename from app/src/main/java/acr/browser/lightning/browser/activity/ThemableBrowserActivity.kt
rename to app/src/main/java/acr/browser/lightning/ThemableBrowserActivity.kt
index 8ed76266a..87887be81 100644
--- a/app/src/main/java/acr/browser/lightning/browser/activity/ThemableBrowserActivity.kt
+++ b/app/src/main/java/acr/browser/lightning/ThemableBrowserActivity.kt
@@ -1,13 +1,10 @@
-package acr.browser.lightning.browser.activity
+package acr.browser.lightning
-import acr.browser.lightning.AppTheme
-import acr.browser.lightning.R
-import acr.browser.lightning.di.injector
+import acr.browser.lightning.browser.di.injector
import acr.browser.lightning.preference.UserPreferences
import acr.browser.lightning.utils.ThemeUtils
import android.content.Intent
import android.graphics.Color
-import android.os.Build
import android.os.Bundle
import android.view.Menu
import androidx.annotation.StyleRes
@@ -17,10 +14,13 @@ import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.iterator
import javax.inject.Inject
+/**
+ * A theme aware activity that updates its theme based on the user preferences.
+ */
abstract class ThemableBrowserActivity : AppCompatActivity() {
- // TODO reduce protected visibility
- @Inject protected lateinit var userPreferences: UserPreferences
+ @Inject
+ internal lateinit var userPreferences: UserPreferences
private var themeId: AppTheme = AppTheme.LIGHT
private var showTabsInDrawer: Boolean = false
@@ -39,11 +39,13 @@ abstract class ThemableBrowserActivity : AppCompatActivity() {
showTabsInDrawer = userPreferences.showTabsInDrawer
// set the theme
- setTheme(provideThemeOverride() ?: when (userPreferences.useTheme) {
- AppTheme.LIGHT -> R.style.Theme_LightTheme
- AppTheme.DARK -> R.style.Theme_DarkTheme
- AppTheme.BLACK -> R.style.Theme_BlackTheme
- })
+ setTheme(
+ provideThemeOverride() ?: when (userPreferences.useTheme) {
+ AppTheme.LIGHT -> R.style.Theme_LightTheme
+ AppTheme.DARK -> R.style.Theme_DarkTheme
+ AppTheme.BLACK -> R.style.Theme_BlackTheme
+ }
+ )
super.onCreate(savedInstanceState)
resetPreferences()
@@ -53,7 +55,12 @@ abstract class ThemableBrowserActivity : AppCompatActivity() {
withStyledAttributes(attrs = intArrayOf(R.attr.iconColorState)) {
val iconTintList = getColorStateList(0)
menu.iterator().forEach { menuItem ->
- menuItem.icon?.let { DrawableCompat.setTintList(DrawableCompat.wrap(it), iconTintList) }
+ menuItem.icon?.let {
+ DrawableCompat.setTintList(
+ DrawableCompat.wrap(it),
+ iconTintList
+ )
+ }
}
}
@@ -61,12 +68,10 @@ abstract class ThemableBrowserActivity : AppCompatActivity() {
}
private fun resetPreferences() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- if (userPreferences.useBlackStatusBar || !userPreferences.showTabsInDrawer) {
- window.statusBarColor = Color.BLACK
- } else {
- window.statusBarColor = ThemeUtils.getStatusBarColor(this)
- }
+ if (userPreferences.useBlackStatusBar || !userPreferences.showTabsInDrawer) {
+ window.statusBarColor = Color.BLACK
+ } else {
+ window.statusBarColor = ThemeUtils.getStatusBarColor(this)
}
}
diff --git a/app/src/main/java/acr/browser/lightning/adblock/BloomFilterAdBlocker.kt b/app/src/main/java/acr/browser/lightning/adblock/BloomFilterAdBlocker.kt
index 1d9634108..cc235eb15 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/BloomFilterAdBlocker.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/BloomFilterAdBlocker.kt
@@ -13,8 +13,8 @@ import acr.browser.lightning.adblock.util.hash.MurmurHashStringAdapter
import acr.browser.lightning.database.adblock.Host
import acr.browser.lightning.database.adblock.HostsRepository
import acr.browser.lightning.database.adblock.HostsRepositoryInfo
-import acr.browser.lightning.di.DatabaseScheduler
-import acr.browser.lightning.di.MainScheduler
+import acr.browser.lightning.browser.di.DatabaseScheduler
+import acr.browser.lightning.browser.di.MainScheduler
import acr.browser.lightning.extensions.toast
import acr.browser.lightning.log.Logger
import android.app.Application
@@ -50,7 +50,8 @@ class BloomFilterAdBlocker @Inject constructor(
) : AdBlocker {
private val bloomFilter: DelegatingBloomFilter
= DelegatingBloomFilter()
- private val objectStore: ObjectStore> = JvmObjectStore(application, MurmurHashStringAdapter())
+ private val objectStore: ObjectStore> =
+ JvmObjectStore(application, MurmurHashStringAdapter())
private val compositeDisposable = CompositeDisposable()
@@ -116,19 +117,20 @@ class BloomFilterAdBlocker @Inject constructor(
objectStore.retrieve(BLOOM_FILTER_KEY)
}
- private fun createAndSaveBloomFilter(hosts: List): Single> = Single.fromCallable {
- logger.log(TAG, "Constructing bloom filter from list")
+ private fun createAndSaveBloomFilter(hosts: List): Single> =
+ Single.fromCallable {
+ logger.log(TAG, "Constructing bloom filter from list")
- val bloomFilter = DefaultBloomFilter(
- numberOfElements = hosts.size,
- falsePositiveRate = 0.01,
- hashingAlgorithm = MurmurHashHostAdapter()
- )
- bloomFilter.putAll(hosts)
- objectStore.store(BLOOM_FILTER_KEY, bloomFilter)
+ val bloomFilter = DefaultBloomFilter(
+ numberOfElements = hosts.size,
+ falsePositiveRate = 0.01,
+ hashingAlgorithm = MurmurHashHostAdapter()
+ )
+ bloomFilter.putAll(hosts)
+ objectStore.store(BLOOM_FILTER_KEY, bloomFilter)
- bloomFilter
- }
+ bloomFilter
+ }
override fun isAd(url: String): Boolean {
val domain = url.host() ?: return false
diff --git a/app/src/main/java/acr/browser/lightning/adblock/allowlist/SessionAllowListModel.kt b/app/src/main/java/acr/browser/lightning/adblock/allowlist/SessionAllowListModel.kt
index 6531dd9cd..783d05178 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/allowlist/SessionAllowListModel.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/allowlist/SessionAllowListModel.kt
@@ -2,7 +2,7 @@ package acr.browser.lightning.adblock.allowlist
import acr.browser.lightning.database.allowlist.AdBlockAllowListRepository
import acr.browser.lightning.database.allowlist.AllowListEntry
-import acr.browser.lightning.di.DatabaseScheduler
+import acr.browser.lightning.browser.di.DatabaseScheduler
import acr.browser.lightning.log.Logger
import androidx.core.net.toUri
import io.reactivex.Completable
diff --git a/app/src/main/java/acr/browser/lightning/adblock/parser/HostsFileParser.kt b/app/src/main/java/acr/browser/lightning/adblock/parser/HostsFileParser.kt
index 7dcc4d8cc..936cb072d 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/parser/HostsFileParser.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/parser/HostsFileParser.kt
@@ -1,14 +1,21 @@
package acr.browser.lightning.adblock.parser
import acr.browser.lightning.database.adblock.Host
-import acr.browser.lightning.extensions.*
+import acr.browser.lightning.extensions.containsChar
+import acr.browser.lightning.extensions.indexOfChar
+import acr.browser.lightning.extensions.inlineReplace
+import acr.browser.lightning.extensions.inlineReplaceChar
+import acr.browser.lightning.extensions.inlineTrim
+import acr.browser.lightning.extensions.stringEquals
+import acr.browser.lightning.extensions.substringToBuilder
import acr.browser.lightning.log.Logger
import java.io.InputStreamReader
+import javax.inject.Inject
/**
* A single threaded parser for a hosts file.
*/
-class HostsFileParser(
+class HostsFileParser @Inject constructor(
private val logger: Logger
) {
diff --git a/app/src/main/java/acr/browser/lightning/adblock/source/AssetsHostsDataSource.kt b/app/src/main/java/acr/browser/lightning/adblock/source/AssetsHostsDataSource.kt
index 557fd2cba..b1fff8155 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/source/AssetsHostsDataSource.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/source/AssetsHostsDataSource.kt
@@ -6,15 +6,19 @@ import acr.browser.lightning.log.Logger
import android.content.res.AssetManager
import io.reactivex.Single
import java.io.InputStreamReader
+import javax.inject.Inject
+import javax.inject.Provider
/**
* A [HostsDataSource] that reads from the hosts list in assets.
*
* @param assetManager The store for application assets.
+ * @param hostsFileParserProvider The provider used to construct the parser.
* @param logger The logger used to log status.
*/
-class AssetsHostsDataSource constructor(
+class AssetsHostsDataSource @Inject constructor(
private val assetManager: AssetManager,
+ private val hostsFileParserProvider: Provider,
private val logger: Logger
) : HostsDataSource {
@@ -28,7 +32,7 @@ class AssetsHostsDataSource constructor(
*/
override fun loadHosts(): Single = Single.create { emitter ->
val reader = InputStreamReader(assetManager.open(BLOCKED_DOMAINS_LIST_FILE_NAME))
- val hostsFileParser = HostsFileParser(logger)
+ val hostsFileParser = hostsFileParserProvider.get()
val domains = hostsFileParser.parseInput(reader)
diff --git a/app/src/main/java/acr/browser/lightning/adblock/source/FileHostsDataSource.kt b/app/src/main/java/acr/browser/lightning/adblock/source/FileHostsDataSource.kt
index eda1ed21e..2e90e5876 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/source/FileHostsDataSource.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/source/FileHostsDataSource.kt
@@ -5,6 +5,9 @@ import acr.browser.lightning.adblock.util.hash.computeMD5
import acr.browser.lightning.extensions.onIOExceptionResumeNext
import acr.browser.lightning.log.Logger
import acr.browser.lightning.preference.UserPreferences
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import io.reactivex.Single
import java.io.File
import java.io.InputStreamReader
@@ -15,9 +18,9 @@ import java.io.InputStreamReader
* @param logger The logger used to log information about the loading process.
* @param file The file from which hosts will be loaded. Must have read access to the file.
*/
-class FileHostsDataSource constructor(
+class FileHostsDataSource @AssistedInject constructor(
private val logger: Logger,
- private val file: File
+ @Assisted private val file: File
) : HostsDataSource {
/**
@@ -44,4 +47,15 @@ class FileHostsDataSource constructor(
private const val TAG = "FileHostsDataSource"
}
+ /**
+ * The factory used to construct the data source.
+ */
+ @AssistedFactory
+ interface Factory {
+ /**
+ * Create the data source for the provided file.
+ */
+ fun create(file: File): FileHostsDataSource
+ }
+
}
diff --git a/app/src/main/java/acr/browser/lightning/adblock/source/PreferencesHostsDataSourceProvider.kt b/app/src/main/java/acr/browser/lightning/adblock/source/PreferencesHostsDataSourceProvider.kt
index 99d0b1522..bc06934d5 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/source/PreferencesHostsDataSourceProvider.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/source/PreferencesHostsDataSourceProvider.kt
@@ -1,13 +1,7 @@
package acr.browser.lightning.adblock.source
-import acr.browser.lightning.di.HostsClient
-import acr.browser.lightning.log.Logger
import acr.browser.lightning.preference.UserPreferences
-import android.app.Application
-import android.content.res.AssetManager
import dagger.Reusable
-import io.reactivex.Single
-import okhttp3.OkHttpClient
import javax.inject.Inject
/**
@@ -16,17 +10,15 @@ import javax.inject.Inject
@Reusable
class PreferencesHostsDataSourceProvider @Inject constructor(
private val userPreferences: UserPreferences,
- private val assetManager: AssetManager,
- private val logger: Logger,
- @HostsClient private val okHttpClient: Single,
- private val application: Application
+ private val assetsHostsDataSource: AssetsHostsDataSource,
+ private val fileHostsDataSourceFactory: FileHostsDataSource.Factory,
+ private val urlHostsDataSourceFactory: UrlHostsDataSource.Factory
) : HostsDataSourceProvider {
override fun createHostsDataSource(): HostsDataSource =
when (val source = userPreferences.selectedHostsSource()) {
- HostsSourceType.Default -> AssetsHostsDataSource(assetManager, logger)
- is HostsSourceType.Local -> FileHostsDataSource(logger, source.file)
- is HostsSourceType.Remote -> UrlHostsDataSource(source.httpUrl, okHttpClient, logger, userPreferences, application)
+ HostsSourceType.Default -> assetsHostsDataSource
+ is HostsSourceType.Local -> fileHostsDataSourceFactory.create(source.file)
+ is HostsSourceType.Remote -> urlHostsDataSourceFactory.create(source.httpUrl)
}
-
}
diff --git a/app/src/main/java/acr/browser/lightning/adblock/source/UrlHostsDataSource.kt b/app/src/main/java/acr/browser/lightning/adblock/source/UrlHostsDataSource.kt
index ead73aabe..2f2c3ae5b 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/source/UrlHostsDataSource.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/source/UrlHostsDataSource.kt
@@ -1,22 +1,31 @@
package acr.browser.lightning.adblock.source
import acr.browser.lightning.adblock.parser.HostsFileParser
+import acr.browser.lightning.browser.di.HostsClient
import acr.browser.lightning.extensions.onIOExceptionResumeNext
import acr.browser.lightning.log.Logger
import acr.browser.lightning.preference.UserPreferences
import acr.browser.lightning.preference.userAgent
import android.app.Application
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import io.reactivex.Single
-import okhttp3.*
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
import java.io.IOException
import java.io.InputStreamReader
/**
* A [HostsDataSource] that loads hosts from an [HttpUrl].
*/
-class UrlHostsDataSource(
- private val url: HttpUrl,
- private val okHttpClient: Single,
+class UrlHostsDataSource @AssistedInject constructor(
+ @Assisted private val url: HttpUrl,
+ @HostsClient private val okHttpClient: Single,
private val logger: Logger,
private val userPreferences: UserPreferences,
private val application: Application
@@ -39,8 +48,9 @@ class UrlHostsDataSource(
override fun onResponse(call: Call, response: Response) {
val successfulResponse = response.takeIf(Response::isSuccessful)
?: return emitter.onError(IOException("Error reading remote file"))
- val input = successfulResponse.body()?.byteStream()?.let(::InputStreamReader)
- ?: return emitter.onError(IOException("Empty response"))
+ val input =
+ successfulResponse.body()?.byteStream()?.let(::InputStreamReader)
+ ?: return emitter.onError(IOException("Empty response"))
val hostsFileParser = HostsFileParser(logger)
@@ -59,4 +69,15 @@ class UrlHostsDataSource(
private const val TAG = "UrlHostsDataSource"
}
+ /**
+ * Used to create the data source.
+ */
+ @AssistedFactory
+ interface Factory {
+ /**
+ * Create the data source for the provided URL.
+ */
+ fun create(url: HttpUrl): UrlHostsDataSource
+ }
+
}
diff --git a/app/src/main/java/acr/browser/lightning/adblock/util/DefaultBloomFilter.kt b/app/src/main/java/acr/browser/lightning/adblock/util/DefaultBloomFilter.kt
index 07fd7eca6..211ed0a84 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/util/DefaultBloomFilter.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/util/DefaultBloomFilter.kt
@@ -4,7 +4,7 @@ import acr.browser.lightning.adblock.util.hash.HashingAlgorithm
import acr.browser.lightning.adblock.util.integer.lowerHalf
import acr.browser.lightning.adblock.util.integer.upperHalf
import java.io.Serializable
-import java.util.*
+import java.util.BitSet
import kotlin.math.ln
import kotlin.math.roundToInt
@@ -38,9 +38,10 @@ class DefaultBloomFilter(
private val hashingAlgorithm: HashingAlgorithm
) : BloomFilter, Serializable {
- private val numberOfBits: Int = (-numberOfElements * ln(falsePositiveRate) / (ln(2.0) * ln(2.0)))
- .roundToInt()
- .coerceAtLeast(1)
+ private val numberOfBits: Int =
+ (-numberOfElements * ln(falsePositiveRate) / (ln(2.0) * ln(2.0)))
+ .roundToInt()
+ .coerceAtLeast(1)
private val numberOfHashes: Int = (numberOfBits * ln(2.0) / numberOfElements)
.roundToInt()
diff --git a/app/src/main/java/acr/browser/lightning/adblock/util/DelegatingBloomFilter.kt b/app/src/main/java/acr/browser/lightning/adblock/util/DelegatingBloomFilter.kt
index 352ef6b89..6cde4d15f 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/util/DelegatingBloomFilter.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/util/DelegatingBloomFilter.kt
@@ -5,9 +5,11 @@ package acr.browser.lightning.adblock.util
*/
class DelegatingBloomFilter(var delegate: BloomFilter? = null) : BloomFilter {
- override fun put(item: T) = throw IllegalStateException("DelegatingBloomFilter does not support put")
+ override fun put(item: T) =
+ throw IllegalStateException("DelegatingBloomFilter does not support put")
- override fun putAll(collection: Collection) = throw IllegalStateException("DelegatingBloomFilter does not support putAll")
+ override fun putAll(collection: Collection) =
+ throw IllegalStateException("DelegatingBloomFilter does not support putAll")
override fun mightContain(item: T): Boolean = delegate?.mightContain(item) ?: false
diff --git a/app/src/main/java/acr/browser/lightning/adblock/util/hash/FileHash.kt b/app/src/main/java/acr/browser/lightning/adblock/util/hash/FileHash.kt
index 20f2bb89c..dbca65294 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/util/hash/FileHash.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/util/hash/FileHash.kt
@@ -2,7 +2,7 @@ package acr.browser.lightning.adblock.util.hash
import java.io.InputStream
import java.security.MessageDigest
-import java.util.*
+import java.util.Locale
/**
* Compute and return the MD5 hash of the [InputStream].
diff --git a/app/src/main/java/acr/browser/lightning/adblock/util/object/JvmObjectStore.kt b/app/src/main/java/acr/browser/lightning/adblock/util/object/JvmObjectStore.kt
index 7acbffaf3..33d01b04d 100644
--- a/app/src/main/java/acr/browser/lightning/adblock/util/object/JvmObjectStore.kt
+++ b/app/src/main/java/acr/browser/lightning/adblock/util/object/JvmObjectStore.kt
@@ -3,7 +3,12 @@ package acr.browser.lightning.adblock.util.`object`
import acr.browser.lightning.adblock.util.hash.HashingAlgorithm
import acr.browser.lightning.extensions.safeUse
import android.app.Application
-import java.io.*
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.io.Serializable
/**
* An [ObjectStore] that serializes objects using the [ObjectInputStream].
diff --git a/app/src/main/java/acr/browser/lightning/bookmark/NetscapeBookmarkFormatImporter.kt b/app/src/main/java/acr/browser/lightning/bookmark/NetscapeBookmarkFormatImporter.kt
index 7531b97f4..d922c85de 100644
--- a/app/src/main/java/acr/browser/lightning/bookmark/NetscapeBookmarkFormatImporter.kt
+++ b/app/src/main/java/acr/browser/lightning/bookmark/NetscapeBookmarkFormatImporter.kt
@@ -36,12 +36,14 @@ class NetscapeBookmarkFormatImporter @Inject constructor() : BookmarkImporter {
immediateChild.nextElementSibling()
.processFolder(computeFolderName(folderName, immediateChild.text()))
immediateChild.isTag(BOOKMARK_TAG) ->
- listOf(Bookmark.Entry(
- url = immediateChild.attr(HREF),
- title = immediateChild.text(),
- position = 0,
- folder = folderName.asFolder()
- ))
+ listOf(
+ Bookmark.Entry(
+ url = immediateChild.attr(HREF),
+ title = immediateChild.text(),
+ position = 0,
+ folder = folderName.asFolder()
+ )
+ )
else -> emptyList()
}
}
diff --git a/app/src/main/java/acr/browser/lightning/browser/BookmarksView.kt b/app/src/main/java/acr/browser/lightning/browser/BookmarksView.kt
deleted file mode 100644
index b44ad0f66..000000000
--- a/app/src/main/java/acr/browser/lightning/browser/BookmarksView.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package acr.browser.lightning.browser
-
-import acr.browser.lightning.database.Bookmark
-
-interface BookmarksView {
-
- fun navigateBack()
-
- fun handleUpdatedUrl(url: String)
-
- fun handleBookmarkDeleted(bookmark: Bookmark)
-
-}
diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserActivity.kt b/app/src/main/java/acr/browser/lightning/browser/BrowserActivity.kt
new file mode 100644
index 000000000..92679c2db
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/BrowserActivity.kt
@@ -0,0 +1,768 @@
+package acr.browser.lightning.browser
+
+import acr.browser.lightning.AppTheme
+import acr.browser.lightning.R
+import acr.browser.lightning.ThemableBrowserActivity
+import acr.browser.lightning.animation.AnimationUtils
+import acr.browser.lightning.browser.bookmark.BookmarkRecyclerViewAdapter
+import acr.browser.lightning.browser.color.ColorAnimator
+import acr.browser.lightning.browser.image.ImageLoader
+import acr.browser.lightning.browser.keys.KeyEventAdapter
+import acr.browser.lightning.browser.menu.MenuItemAdapter
+import acr.browser.lightning.browser.search.IntentExtractor
+import acr.browser.lightning.browser.search.SearchListener
+import acr.browser.lightning.browser.ui.BookmarkConfiguration
+import acr.browser.lightning.browser.ui.TabConfiguration
+import acr.browser.lightning.browser.ui.UiConfiguration
+import acr.browser.lightning.browser.search.StyleRemovingTextWatcher
+import acr.browser.lightning.constant.HTTP
+import acr.browser.lightning.database.Bookmark
+import acr.browser.lightning.database.HistoryEntry
+import acr.browser.lightning.database.SearchSuggestion
+import acr.browser.lightning.database.WebPage
+import acr.browser.lightning.database.downloads.DownloadEntry
+import acr.browser.lightning.databinding.BrowserActivityBinding
+import acr.browser.lightning.browser.di.injector
+import acr.browser.lightning.browser.tab.DesktopTabRecyclerViewAdapter
+import acr.browser.lightning.browser.tab.DrawerTabRecyclerViewAdapter
+import acr.browser.lightning.browser.tab.TabPager
+import acr.browser.lightning.browser.tab.TabViewHolder
+import acr.browser.lightning.browser.tab.TabViewState
+import acr.browser.lightning.dialog.BrowserDialog
+import acr.browser.lightning.dialog.DialogItem
+import acr.browser.lightning.dialog.LightningDialogBuilder
+import acr.browser.lightning.search.SuggestionsAdapter
+import acr.browser.lightning.ssl.createSslDrawableForState
+import acr.browser.lightning.utils.ProxyUtils
+import acr.browser.lightning.utils.value
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.inputmethod.InputMethodManager
+import android.widget.AdapterView
+import android.widget.ImageView
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.DrawableRes
+import androidx.annotation.MenuRes
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.SimpleItemAnimator
+import acr.browser.lightning.browser.view.targetUrl.LongPress
+import acr.browser.lightning.extensions.color
+import acr.browser.lightning.extensions.drawable
+import acr.browser.lightning.extensions.resizeAndShow
+import acr.browser.lightning.extensions.takeIfInstance
+import acr.browser.lightning.extensions.tint
+import android.view.Gravity
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.WindowManager
+import javax.inject.Inject
+
+/**
+ * The base browser activity that governs the browsing experience for both default and incognito
+ * browsers.
+ */
+abstract class BrowserActivity : ThemableBrowserActivity() {
+
+ private lateinit var binding: BrowserActivityBinding
+ private lateinit var tabsAdapter: ListAdapter
+ private lateinit var bookmarksAdapter: BookmarkRecyclerViewAdapter
+
+ private var menuItemShare: MenuItem? = null
+ private var menuItemCopyLink: MenuItem? = null
+ private var menuItemAddToHome: MenuItem? = null
+ private var menuItemAddBookmark: MenuItem? = null
+ private var menuItemReaderMode: MenuItem? = null
+
+ private val defaultColor by lazy { color(R.color.primary_color) }
+ private val backgroundDrawable by lazy { ColorDrawable(defaultColor) }
+
+ private var customView: View? = null
+
+ @Suppress("ConvertLambdaToReference")
+ private val launcher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { presenter.onFileChooserResult(it) }
+
+ @Inject
+ internal lateinit var imageLoader: ImageLoader
+
+ @Inject
+ internal lateinit var keyEventAdapter: KeyEventAdapter
+
+ @Inject
+ internal lateinit var menuItemAdapter: MenuItemAdapter
+
+ @Inject
+ internal lateinit var inputMethodManager: InputMethodManager
+
+ @Inject
+ internal lateinit var presenter: BrowserPresenter
+
+ @Inject
+ internal lateinit var tabPager: TabPager
+
+ @Inject
+ internal lateinit var intentExtractor: IntentExtractor
+
+ @Inject
+ internal lateinit var lightningDialogBuilder: LightningDialogBuilder
+
+ @Inject
+ internal lateinit var uiConfiguration: UiConfiguration
+
+ @Inject
+ internal lateinit var proxyUtils: ProxyUtils
+
+ /**
+ * True if the activity is operating in incognito mode, false otherwise.
+ */
+ abstract fun isIncognito(): Boolean
+
+ /**
+ * Provide the menu used by the browser instance.
+ */
+ @MenuRes
+ abstract fun menu(): Int
+
+ /**
+ * Provide the home icon used by the browser instance.
+ */
+ @DrawableRes
+ abstract fun homeIcon(): Int
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = BrowserActivityBinding.inflate(LayoutInflater.from(this))
+
+ setContentView(binding.root)
+ setSupportActionBar(binding.toolbar)
+
+ injector.browser2ComponentBuilder()
+ .activity(this)
+ .browserFrame(binding.contentFrame)
+ .toolbarRoot(binding.uiLayout)
+ .toolbar(binding.toolbarLayout)
+ .initialIntent(intent)
+ .incognitoMode(isIncognito())
+ .build()
+ .inject(this)
+
+ binding.drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
+
+ override fun onDrawerOpened(drawerView: View) {
+ if (drawerView == binding.tabDrawer) {
+ presenter.onTabDrawerMoved(isOpen = true)
+ } else if (drawerView == binding.bookmarkDrawer) {
+ presenter.onBookmarkDrawerMoved(isOpen = true)
+ }
+ }
+
+ override fun onDrawerClosed(drawerView: View) {
+ if (drawerView == binding.tabDrawer) {
+ presenter.onTabDrawerMoved(isOpen = false)
+ } else if (drawerView == binding.bookmarkDrawer) {
+ presenter.onBookmarkDrawerMoved(isOpen = false)
+ }
+ }
+ })
+
+ binding.bookmarkDrawer.layoutParams =
+ (binding.bookmarkDrawer.layoutParams as DrawerLayout.LayoutParams).apply {
+ gravity = when (uiConfiguration.bookmarkConfiguration) {
+ BookmarkConfiguration.LEFT -> Gravity.START
+ BookmarkConfiguration.RIGHT -> Gravity.END
+ }
+ }
+
+ binding.tabDrawer.layoutParams =
+ (binding.tabDrawer.layoutParams as DrawerLayout.LayoutParams).apply {
+ gravity = when (uiConfiguration.bookmarkConfiguration) {
+ BookmarkConfiguration.LEFT -> Gravity.END
+ BookmarkConfiguration.RIGHT -> Gravity.START
+ }
+ }
+
+ binding.homeImageView.isVisible =
+ uiConfiguration.tabConfiguration == TabConfiguration.DESKTOP || isIncognito()
+ binding.homeImageView.setImageResource(homeIcon())
+ binding.tabCountView.isVisible =
+ uiConfiguration.tabConfiguration == TabConfiguration.DRAWER && !isIncognito()
+
+ if (uiConfiguration.tabConfiguration == TabConfiguration.DESKTOP) {
+ binding.drawerLayout.setDrawerLockMode(
+ DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
+ binding.tabDrawer
+ )
+ }
+
+ if (uiConfiguration.tabConfiguration == TabConfiguration.DRAWER) {
+ tabsAdapter = DrawerTabRecyclerViewAdapter(
+ onClick = presenter::onTabClick,
+ onCloseClick = presenter::onTabClose,
+ onLongClick = presenter::onTabLongClick
+ )
+ binding.drawerTabsList.isVisible = true
+ binding.drawerTabsList.adapter = tabsAdapter
+ binding.drawerTabsList.layoutManager = LinearLayoutManager(this)
+ binding.desktopTabsList.isVisible = false
+ } else {
+ tabsAdapter = DesktopTabRecyclerViewAdapter(
+ context = this,
+ onClick = presenter::onTabClick,
+ onCloseClick = presenter::onTabClose,
+ onLongClick = presenter::onTabLongClick
+ )
+ binding.desktopTabsList.isVisible = true
+ binding.desktopTabsList.adapter = tabsAdapter
+ binding.desktopTabsList.layoutManager =
+ LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
+ binding.desktopTabsList.itemAnimator?.takeIfInstance()
+ ?.supportsChangeAnimations = false
+ binding.drawerTabsList.isVisible = false
+ }
+
+ bookmarksAdapter = BookmarkRecyclerViewAdapter(
+ onClick = presenter::onBookmarkClick,
+ onLongClick = presenter::onBookmarkLongClick,
+ imageLoader = imageLoader
+ )
+ binding.bookmarkListView.adapter = bookmarksAdapter
+ binding.bookmarkListView.layoutManager = LinearLayoutManager(this)
+
+ presenter.onViewAttached(BrowserStateAdapter(this))
+
+ val suggestionsAdapter = SuggestionsAdapter(this, isIncognito = isIncognito()).apply {
+ onSuggestionInsertClick = {
+ if (it is SearchSuggestion) {
+ binding.search.setText(it.title)
+ binding.search.setSelection(it.title.length)
+ } else {
+ binding.search.setText(it.url)
+ binding.search.setSelection(it.url.length)
+ }
+ }
+ }
+ binding.search.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
+ binding.search.clearFocus()
+ presenter.onSearchSuggestionClicked(suggestionsAdapter.getItem(position) as WebPage)
+ inputMethodManager.hideSoftInputFromWindow(binding.root.windowToken, 0)
+ }
+ binding.search.setAdapter(suggestionsAdapter)
+ val searchListener = SearchListener(
+ onConfirm = { presenter.onSearch(binding.search.text.toString()) },
+ inputMethodManager = inputMethodManager
+ )
+ binding.search.setOnEditorActionListener(searchListener)
+ binding.search.setOnKeyListener(searchListener)
+ binding.search.addTextChangedListener(StyleRemovingTextWatcher())
+ binding.search.setOnFocusChangeListener { _, hasFocus ->
+ presenter.onSearchFocusChanged(hasFocus)
+ binding.search.selectAll()
+ }
+
+ binding.findPrevious.setOnClickListener { presenter.onFindPrevious() }
+ binding.findNext.setOnClickListener { presenter.onFindNext() }
+ binding.findQuit.setOnClickListener { presenter.onFindDismiss() }
+
+ binding.homeButton.setOnClickListener { presenter.onTabCountViewClick() }
+ binding.actionBack.setOnClickListener { presenter.onBackClick() }
+ binding.actionForward.setOnClickListener { presenter.onForwardClick() }
+ binding.actionHome.setOnClickListener { presenter.onHomeClick() }
+ binding.newTabButton.setOnClickListener { presenter.onNewTabClick() }
+ binding.newTabButton.setOnLongClickListener {
+ presenter.onNewTabLongClick()
+ true
+ }
+ binding.searchRefresh.setOnClickListener { presenter.onRefreshOrStopClick() }
+ binding.actionAddBookmark.setOnClickListener { presenter.onStarClick() }
+ binding.actionPageTools.setOnClickListener { presenter.onToolsClick() }
+ binding.tabHeaderButton.setOnClickListener { presenter.onTabMenuClick() }
+ binding.actionReading.setOnClickListener { presenter.onReadingModeClick() }
+ binding.bookmarkBackButton.setOnClickListener { presenter.onBookmarkMenuClick() }
+ binding.searchSslStatus.setOnClickListener { presenter.onSslIconClick() }
+
+ tabPager.longPressListener = presenter::onPageLongPress
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ intent?.let(intentExtractor::extractUrlFromIntent)?.let(presenter::onNewAction)
+ super.onNewIntent(intent)
+
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ presenter.onViewDetached()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ presenter.onViewHidden()
+ }
+
+ override fun onBackPressed() {
+ presenter.onNavigateBack()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(menu(), menu)
+ menuItemShare = menu.findItem(R.id.action_share)
+ menuItemCopyLink = menu.findItem(R.id.action_copy)
+ menuItemAddToHome = menu.findItem(R.id.action_add_to_homescreen)
+ menuItemAddBookmark = menu.findItem(R.id.action_add_bookmark)
+ menuItemReaderMode = menu.findItem(R.id.action_reading_mode)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return menuItemAdapter.adaptMenuItem(item)?.let(presenter::onMenuClick)?.let { true }
+ ?: super.onOptionsItemSelected(item)
+ }
+
+ override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
+ return keyEventAdapter.adaptKeyEvent(event)?.let(presenter::onKeyComboClick)?.let { true }
+ ?: super.onKeyUp(keyCode, event)
+ }
+
+ /**
+ * @see BrowserContract.View.renderState
+ */
+ fun renderState(viewState: PartialBrowserViewState) {
+ viewState.isBackEnabled?.let { binding.actionBack.isEnabled = it }
+ viewState.isForwardEnabled?.let { binding.actionForward.isEnabled = it }
+ viewState.displayUrl?.let(binding.search::setText)
+ viewState.sslState?.let {
+ binding.searchSslStatus.setImageDrawable(createSslDrawableForState(it))
+ binding.searchSslStatus.updateVisibilityForDrawable()
+ }
+ viewState.enableFullMenu?.let {
+ menuItemShare?.isVisible = it
+ menuItemCopyLink?.isVisible = it
+ menuItemAddToHome?.isVisible = it
+ menuItemAddBookmark?.isVisible = it
+ menuItemReaderMode?.isVisible = it
+ }
+ viewState.themeColor?.value()?.let(::animateColorChange)
+ viewState.progress?.let { binding.progressView.progress = it }
+ viewState.isRefresh?.let {
+ binding.searchRefresh.setImageResource(
+ if (it) {
+ R.drawable.ic_action_refresh
+ } else {
+ R.drawable.ic_action_delete
+ }
+ )
+ }
+ viewState.bookmarks?.let(bookmarksAdapter::submitList)
+ viewState.isBookmarked?.let { binding.actionAddBookmark.isSelected = it }
+ viewState.isBookmarkEnabled?.let { binding.actionAddBookmark.isEnabled = it }
+ viewState.isRootFolder?.let {
+ binding.bookmarkBackButton.startAnimation(
+ AnimationUtils.createRotationTransitionAnimation(
+ binding.bookmarkBackButton,
+ if (it) {
+ R.drawable.ic_action_star
+ } else {
+ R.drawable.ic_action_back
+ }
+ )
+ )
+ }
+ viewState.findInPage?.let {
+ if (it.isEmpty()) {
+ binding.findBar.isVisible = false
+ } else {
+ binding.findBar.isVisible = true
+ binding.findQuery.text = it
+ }
+ }
+ }
+
+ /**
+ * @see BrowserContract.View.renderTabs
+ */
+ fun renderTabs(tabListState: List) {
+ binding.tabCountView.updateCount(tabListState.size)
+ tabsAdapter.submitList(tabListState)
+ }
+
+ /**
+ * @see BrowserContract.View.showAddBookmarkDialog
+ */
+ fun showAddBookmarkDialog(title: String, url: String, folders: List) {
+ lightningDialogBuilder.showAddBookmarkDialog(
+ activity = this,
+ currentTitle = title,
+ currentUrl = url,
+ folders = folders,
+ onSave = presenter::onBookmarkConfirmed
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showBookmarkOptionsDialog
+ */
+ fun showBookmarkOptionsDialog(bookmark: Bookmark.Entry) {
+ lightningDialogBuilder.showLongPressedDialogForBookmarkUrl(
+ activity = this,
+ onClick = {
+ presenter.onBookmarkOptionClick(bookmark, it)
+ }
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showEditBookmarkDialog
+ */
+ fun showEditBookmarkDialog(title: String, url: String, folder: String, folders: List) {
+ lightningDialogBuilder.showEditBookmarkDialog(
+ activity = this,
+ currentTitle = title,
+ currentUrl = url,
+ currentFolder = folder,
+ folders = folders,
+ onSave = presenter::onBookmarkEditConfirmed
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showFolderOptionsDialog
+ */
+ fun showFolderOptionsDialog(folder: Bookmark.Folder) {
+ lightningDialogBuilder.showBookmarkFolderLongPressedDialog(
+ activity = this,
+ onClick = {
+ presenter.onFolderOptionClick(folder, it)
+ }
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showEditFolderDialog
+ */
+ fun showEditFolderDialog(oldTitle: String) {
+ lightningDialogBuilder.showRenameFolderDialog(
+ activity = this,
+ oldTitle = oldTitle,
+ onSave = presenter::onBookmarkFolderRenameConfirmed
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showDownloadOptionsDialog
+ */
+ fun showDownloadOptionsDialog(download: DownloadEntry) {
+ lightningDialogBuilder.showLongPressedDialogForDownloadUrl(
+ activity = this,
+ onClick = {
+ presenter.onDownloadOptionClick(download, it)
+ }
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showHistoryOptionsDialog
+ */
+ fun showHistoryOptionsDialog(historyEntry: HistoryEntry) {
+ lightningDialogBuilder.showLongPressedHistoryLinkDialog(
+ activity = this,
+ onClick = {
+ presenter.onHistoryOptionClick(historyEntry, it)
+ }
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showFindInPageDialog
+ */
+ fun showFindInPageDialog() {
+ BrowserDialog.showEditText(
+ this,
+ R.string.action_find,
+ R.string.search_hint,
+ R.string.search_hint,
+ presenter::onFindInPage
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showLinkLongPressDialog
+ */
+ fun showLinkLongPressDialog(longPress: LongPress) {
+ BrowserDialog.show(this, longPress.targetUrl?.replace(HTTP, ""),
+ DialogItem(title = R.string.dialog_open_new_tab) {
+ presenter.onLinkLongPressEvent(
+ longPress,
+ BrowserContract.LinkLongPressEvent.NEW_TAB
+ )
+ },
+ DialogItem(title = R.string.dialog_open_background_tab) {
+ presenter.onLinkLongPressEvent(
+ longPress,
+ BrowserContract.LinkLongPressEvent.BACKGROUND_TAB
+ )
+ },
+ DialogItem(
+ title = R.string.dialog_open_incognito_tab,
+ isConditionMet = !isIncognito()
+ ) {
+ presenter.onLinkLongPressEvent(
+ longPress,
+ BrowserContract.LinkLongPressEvent.INCOGNITO_TAB
+ )
+ },
+ DialogItem(title = R.string.action_share) {
+ presenter.onLinkLongPressEvent(longPress, BrowserContract.LinkLongPressEvent.SHARE)
+ },
+ DialogItem(title = R.string.dialog_copy_link) {
+ presenter.onLinkLongPressEvent(
+ longPress,
+ BrowserContract.LinkLongPressEvent.COPY_LINK
+ )
+ })
+ }
+
+ /**
+ * @see BrowserContract.View.showImageLongPressDialog
+ */
+ fun showImageLongPressDialog(longPress: LongPress) {
+ BrowserDialog.show(this, longPress.targetUrl?.replace(HTTP, ""),
+ DialogItem(title = R.string.dialog_open_new_tab) {
+ presenter.onImageLongPressEvent(
+ longPress,
+ BrowserContract.ImageLongPressEvent.NEW_TAB
+ )
+ },
+ DialogItem(title = R.string.dialog_open_background_tab) {
+ presenter.onImageLongPressEvent(
+ longPress,
+ BrowserContract.ImageLongPressEvent.BACKGROUND_TAB
+ )
+ },
+ DialogItem(
+ title = R.string.dialog_open_incognito_tab,
+ isConditionMet = !isIncognito()
+ ) {
+ presenter.onImageLongPressEvent(
+ longPress,
+ BrowserContract.ImageLongPressEvent.INCOGNITO_TAB
+ )
+ },
+ DialogItem(title = R.string.action_share) {
+ presenter.onImageLongPressEvent(
+ longPress,
+ BrowserContract.ImageLongPressEvent.SHARE
+ )
+ },
+ DialogItem(title = R.string.dialog_copy_link) {
+ presenter.onImageLongPressEvent(
+ longPress,
+ BrowserContract.ImageLongPressEvent.COPY_LINK
+ )
+ },
+ DialogItem(title = R.string.dialog_download_image) {
+ presenter.onImageLongPressEvent(
+ longPress,
+ BrowserContract.ImageLongPressEvent.DOWNLOAD
+ )
+ })
+ }
+
+ /**
+ * @see BrowserContract.View.showCloseBrowserDialog
+ */
+ fun showCloseBrowserDialog(id: Int) {
+ BrowserDialog.show(
+ this, R.string.dialog_title_close_browser,
+ DialogItem(title = R.string.close_tab) {
+ presenter.onCloseBrowserEvent(id, BrowserContract.CloseTabEvent.CLOSE_CURRENT)
+ },
+ DialogItem(title = R.string.close_other_tabs) {
+ presenter.onCloseBrowserEvent(id, BrowserContract.CloseTabEvent.CLOSE_OTHERS)
+ },
+ DialogItem(title = R.string.close_all_tabs, onClick = {
+ presenter.onCloseBrowserEvent(id, BrowserContract.CloseTabEvent.CLOSE_ALL)
+ })
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.openBookmarkDrawer
+ */
+ fun openBookmarkDrawer() {
+ binding.drawerLayout.closeDrawer(binding.tabDrawer)
+ binding.drawerLayout.openDrawer(binding.bookmarkDrawer)
+ }
+
+ /**
+ * @see BrowserContract.View.closeBookmarkDrawer
+ */
+ fun closeBookmarkDrawer() {
+ binding.drawerLayout.closeDrawer(binding.bookmarkDrawer)
+ }
+
+ /**
+ * @see BrowserContract.View.openTabDrawer
+ */
+ fun openTabDrawer() {
+ binding.drawerLayout.closeDrawer(binding.bookmarkDrawer)
+ binding.drawerLayout.openDrawer(binding.tabDrawer)
+ }
+
+ /**
+ * @see BrowserContract.View.closeTabDrawer
+ */
+ fun closeTabDrawer() {
+ binding.drawerLayout.closeDrawer(binding.tabDrawer)
+ }
+
+ /**
+ * @see BrowserContract.View.showToolbar
+ */
+ fun showToolbar() {
+ tabPager.showToolbar()
+ }
+
+ /**
+ * @see BrowserContract.View.showToolsDialog
+ */
+ fun showToolsDialog(areAdsAllowed: Boolean, shouldShowAdBlockOption: Boolean) {
+ val whitelistString = if (areAdsAllowed) {
+ R.string.dialog_adblock_enable_for_site
+ } else {
+ R.string.dialog_adblock_disable_for_site
+ }
+
+ BrowserDialog.showWithIcons(
+ this, getString(R.string.dialog_tools_title),
+ DialogItem(
+ icon = drawable(R.drawable.ic_action_desktop),
+ title = R.string.dialog_toggle_desktop,
+ onClick = presenter::onToggleDesktopAgent
+ ),
+ DialogItem(
+ icon = drawable(R.drawable.ic_block),
+ colorTint = color(R.color.error_red).takeIf { areAdsAllowed },
+ title = whitelistString,
+ isConditionMet = shouldShowAdBlockOption,
+ onClick = presenter::onToggleAdBlocking
+ )
+ )
+ }
+
+ /**
+ * @see BrowserContract.View.showLocalFileBlockedDialog
+ */
+ fun showLocalFileBlockedDialog() {
+ AlertDialog.Builder(this)
+ .setCancelable(true)
+ .setTitle(R.string.title_warning)
+ .setMessage(R.string.message_blocked_local)
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
+ presenter.onConfirmOpenLocalFile(allow = false)
+ }
+ .setPositiveButton(R.string.action_open) { _, _ ->
+ presenter.onConfirmOpenLocalFile(allow = true)
+ }
+ .setOnCancelListener { presenter.onConfirmOpenLocalFile(allow = false) }
+ .resizeAndShow()
+ }
+
+ /**
+ * @see BrowserContract.View.showFileChooser
+ */
+ fun showFileChooser(intent: Intent) {
+ launcher.launch(intent)
+ }
+
+ /**
+ * @see BrowserContract.View.showCustomView
+ */
+ fun showCustomView(view: View) {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ binding.root.addView(view)
+ customView = view
+ setFullscreen(enabled = true, immersive = true)
+ }
+
+ /**
+ * @see BrowserContract.View.hideCustomView
+ */
+ fun hideCustomView() {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ customView?.let(binding.root::removeView)
+ customView = null
+ setFullscreen(enabled = false, immersive = false)
+ }
+
+ /**
+ * @see BrowserContract.View.clearSearchFocus
+ */
+ fun clearSearchFocus() {
+ binding.search.clearFocus()
+ }
+
+ // TODO: update to use non deprecated flags
+ private fun setFullscreen(enabled: Boolean, immersive: Boolean) {
+ val window = window
+ val decor = window.decorView
+ if (enabled) {
+ if (immersive) {
+ decor.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+ } else {
+ decor.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
+ }
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
+ )
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ decor.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
+ }
+ }
+
+ private fun animateColorChange(color: Int) {
+ if (!userPreferences.colorModeEnabled || userPreferences.useTheme != AppTheme.LIGHT || isIncognito()) {
+ return
+ }
+ val shouldShowTabsInDrawer = userPreferences.showTabsInDrawer
+ val adapter = tabsAdapter as? DesktopTabRecyclerViewAdapter
+ val colorAnimator = ColorAnimator(defaultColor)
+ binding.toolbar.startAnimation(colorAnimator.animateTo(
+ color
+ ) { mainColor, secondaryColor ->
+ if (shouldShowTabsInDrawer) {
+ backgroundDrawable.color = mainColor
+ window.setBackgroundDrawable(backgroundDrawable)
+ } else {
+ adapter?.updateForegroundTabColor(mainColor)
+ }
+ binding.toolbar.setBackgroundColor(mainColor)
+ binding.searchContainer.background?.tint(secondaryColor)
+ })
+ }
+
+ private fun ImageView.updateVisibilityForDrawable() {
+ visibility = if (drawable == null) {
+ View.GONE
+ } else {
+ View.VISIBLE
+ }
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserContract.kt b/app/src/main/java/acr/browser/lightning/browser/BrowserContract.kt
new file mode 100644
index 000000000..7ec0eea9f
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/BrowserContract.kt
@@ -0,0 +1,373 @@
+package acr.browser.lightning.browser
+
+import acr.browser.lightning.browser.download.PendingDownload
+import acr.browser.lightning.browser.tab.TabModel
+import acr.browser.lightning.browser.tab.TabViewState
+import acr.browser.lightning.database.Bookmark
+import acr.browser.lightning.database.HistoryEntry
+import acr.browser.lightning.database.downloads.DownloadEntry
+import acr.browser.lightning.ssl.SslCertificateInfo
+import acr.browser.lightning.browser.tab.TabInitializer
+import android.content.Intent
+import android.graphics.Bitmap
+import io.reactivex.Completable
+import io.reactivex.Maybe
+import io.reactivex.Observable
+import io.reactivex.Single
+import acr.browser.lightning.browser.view.targetUrl.LongPress
+
+/**
+ * The contract for the browser.
+ */
+interface BrowserContract {
+
+ /**
+ * The view that renders the browser state.
+ */
+ interface View {
+
+ /**
+ * Render the [viewState] for the current tab in the browser.
+ */
+ fun renderState(viewState: BrowserViewState)
+
+ /**
+ * Render the [tabs] in the tabs list.
+ */
+ fun renderTabs(tabs: List)
+
+ /**
+ * Show the dialog to add a bookmark for the current page.
+ *
+ * @param title The current title of the page.
+ * @param url The current URL of the page.
+ * @param folders The available folders that the bookmark can be moved to.
+ */
+ fun showAddBookmarkDialog(title: String, url: String, folders: List)
+
+ /**
+ * Show the options dialog for the provided [bookmark].
+ */
+ fun showBookmarkOptionsDialog(bookmark: Bookmark.Entry)
+
+ /**
+ * Show the dialog to edit a bookmark.
+ *
+ * @param title The current title of the bookmark.
+ * @param url The current URL of the bookmark.
+ * @param folder The current folder the bookmark is in.
+ * @param folders The available folders that the bookmark can be moved to.
+ */
+ fun showEditBookmarkDialog(
+ title: String,
+ url: String,
+ folder: String,
+ folders: List
+ )
+
+ /**
+ * Show the options dialog for the provided [folder].
+ */
+ fun showFolderOptionsDialog(folder: Bookmark.Folder)
+
+ /**
+ * Show the edit folder dialog for the folder with the provided [title].
+ */
+ fun showEditFolderDialog(title: String)
+
+ /**
+ * Show the options dialog for the provided [download].
+ */
+ fun showDownloadOptionsDialog(download: DownloadEntry)
+
+ /**
+ * Show the options dialog for the provided [historyEntry].
+ */
+ fun showHistoryOptionsDialog(historyEntry: HistoryEntry)
+
+ /**
+ * Show the dialog that allows the user to search the web page for a query.
+ */
+ fun showFindInPageDialog()
+
+ /**
+ * Show the options menu for long pressing a link in the web page.
+ */
+ fun showLinkLongPressDialog(longPress: LongPress)
+
+ /**
+ * Show the options menu for long pressing an image in the web page.
+ */
+ fun showImageLongPressDialog(longPress: LongPress)
+
+ /**
+ * Show the informational dialog about the SSL certificate info.
+ */
+ fun showSslDialog(sslCertificateInfo: SslCertificateInfo)
+
+ /**
+ * Show the close browser dialog for the dialog with the provide [id].
+ */
+ fun showCloseBrowserDialog(id: Int)
+
+ /**
+ * Open the bookmark drawer if it is closed.
+ */
+ fun openBookmarkDrawer()
+
+ /**
+ * Close the bookmark drawer if it is open.
+ */
+ fun closeBookmarkDrawer()
+
+ /**
+ * Open the tab drawer if it is closed.
+ */
+ fun openTabDrawer()
+
+ /**
+ * Close the tab drawer if it is open.
+ */
+ fun closeTabDrawer()
+
+ /**
+ * Show the toolbar/search box if it has been hidden due to scrolling.
+ */
+ fun showToolbar()
+
+ /**
+ * Show the tools dialog that allows the user to toggle ad blocking and user agent for the
+ * current page.
+ *
+ * @param areAdsAllowed True if ads are currently allowed on the page, false otherwise.
+ * @param shouldShowAdBlockOption True if ad block toggling is available for the current
+ * page.
+ */
+ fun showToolsDialog(areAdsAllowed: Boolean, shouldShowAdBlockOption: Boolean)
+
+ /**
+ * Show a warning to the user that they are about to open a local file in the browser that
+ * could be potentially dangerous.
+ */
+ fun showLocalFileBlockedDialog()
+
+ /**
+ * Show the file chooser with the provided [intent].
+ */
+ fun showFileChooser(intent: Intent)
+
+ /**
+ * Show a custom [view] over everything that will play a video.
+ */
+ fun showCustomView(view: android.view.View)
+
+ /**
+ * Hide the custom view that was previously shown by calling [showCustomView].
+ */
+ fun hideCustomView()
+
+ /**
+ * Clear focus from the search view if it has focus.
+ */
+ fun clearSearchFocus()
+ }
+
+ /**
+ * The model used to manage tabs in the browser.
+ */
+ interface Model {
+
+ /**
+ * Delete the tab with the provided [id].
+ */
+ fun deleteTab(id: Int): Completable
+
+ /**
+ * Delete all open tabs.
+ */
+ fun deleteAllTabs(): Completable
+
+ /**
+ * Create a tab that will be initialized with the [tabInitializer].
+ */
+ fun createTab(tabInitializer: TabInitializer): Single
+
+ /**
+ * Reopen the most recently closed tab if there is a closed tab to re-open.
+ */
+ fun reopenTab(): Maybe
+
+ /**
+ * Select the tab with the provide [id] as the currently viewed tab.
+ */
+ fun selectTab(id: Int): TabModel
+
+ /**
+ * Initialize all tabs that were previously frozen when the browser was last open, and
+ * initialize any tabs that should be opened from the initial browser action.
+ */
+ fun initializeTabs(): Maybe>
+
+ /**
+ * Notifies the model that all tabs need to be frozen before the browser shuts down.
+ */
+ fun freeze()
+
+ /**
+ * Clean all permanent stored content.
+ */
+ fun clean()
+
+ /**
+ * The current open tabs.
+ */
+ val tabsList: List
+
+ /**
+ * Changes to the current open tabs.
+ */
+ fun tabsListChanges(): Observable>
+
+ }
+
+ /**
+ * Used by the browser to navigate between screens and perform other navigation events.
+ */
+ interface Navigator {
+
+ /**
+ * Open the browser settings screen.
+ */
+ fun openSettings()
+
+ /**
+ * Open the reader mode and load the provided [url].
+ */
+ fun openReaderMode(url: String)
+
+ /**
+ * Share the web page with the provided [url] and [title].
+ */
+ fun sharePage(url: String, title: String?)
+
+ /**
+ * Copy the page [url] to the clip board.
+ */
+ fun copyPageLink(url: String)
+
+ /**
+ * Close the browser and terminate the session.
+ */
+ fun closeBrowser()
+
+ /**
+ * Add a shortcut to the home screen that opens the [url]. Use the provided [title] and
+ * [favicon] to create the shortcut.
+ */
+ fun addToHomeScreen(url: String, title: String, favicon: Bitmap?)
+
+ /**
+ * Download the file provided by the [pendingDownload].
+ */
+ fun download(pendingDownload: PendingDownload)
+
+ /**
+ * Move the browser to the background without terminating the session.
+ */
+ fun backgroundBrowser()
+
+ /**
+ * launch the incognito browser and load the provided [url].
+ */
+ fun launchIncognito(url: String?)
+ }
+
+ /**
+ * The options for the close tab menu dialog.
+ */
+ enum class CloseTabEvent {
+ CLOSE_CURRENT,
+ CLOSE_OTHERS,
+ CLOSE_ALL
+ }
+
+ /**
+ * The options for the bookmark menu dialog.
+ */
+ enum class BookmarkOptionEvent {
+ NEW_TAB,
+ BACKGROUND_TAB,
+ INCOGNITO_TAB,
+ SHARE,
+ COPY_LINK,
+ REMOVE,
+ EDIT
+ }
+
+ /**
+ * The options for the history menu dialog.
+ */
+ enum class HistoryOptionEvent {
+ NEW_TAB,
+ BACKGROUND_TAB,
+ INCOGNITO_TAB,
+ SHARE,
+ COPY_LINK,
+ REMOVE,
+ }
+
+ /**
+ * The options for the download menu dialog.
+ */
+ enum class DownloadOptionEvent {
+ DELETE,
+ DELETE_ALL
+ }
+
+ /**
+ * The options for the folder menu dialog.
+ */
+ enum class FolderOptionEvent {
+ RENAME,
+ REMOVE
+ }
+
+ /**
+ * The options for the link long press menu dialog.
+ */
+ enum class LinkLongPressEvent {
+ NEW_TAB,
+ BACKGROUND_TAB,
+ INCOGNITO_TAB,
+ SHARE,
+ COPY_LINK
+ }
+
+ /**
+ * The options for the image long press menu dialog.
+ */
+ enum class ImageLongPressEvent {
+ NEW_TAB,
+ BACKGROUND_TAB,
+ INCOGNITO_TAB,
+ SHARE,
+ COPY_LINK,
+ DOWNLOAD
+ }
+
+ /**
+ * Supported actions that can be passed to the browser.
+ */
+ sealed class Action {
+ /**
+ * The action to load the provided [url].
+ */
+ data class LoadUrl(val url: String) : Action()
+
+ /**
+ * The action to emergency clean the entire browser contents and stored data.
+ */
+ object Panic : Action()
+ }
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserNavigator.kt b/app/src/main/java/acr/browser/lightning/browser/BrowserNavigator.kt
new file mode 100644
index 000000000..35ea07500
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/BrowserNavigator.kt
@@ -0,0 +1,82 @@
+package acr.browser.lightning.browser
+
+import acr.browser.lightning.IncognitoBrowserActivity
+import acr.browser.lightning.R
+import acr.browser.lightning.browser.cleanup.ExitCleanup
+import acr.browser.lightning.browser.download.DownloadPermissionsHelper
+import acr.browser.lightning.browser.download.PendingDownload
+import acr.browser.lightning.extensions.copyToClipboard
+import acr.browser.lightning.extensions.snackbar
+import acr.browser.lightning.log.Logger
+import acr.browser.lightning.reading.activity.ReadingActivity
+import acr.browser.lightning.settings.activity.SettingsActivity
+import acr.browser.lightning.utils.IntentUtils
+import acr.browser.lightning.utils.Utils
+import android.app.Activity
+import android.content.ClipboardManager
+import android.content.Intent
+import android.graphics.Bitmap
+import javax.inject.Inject
+
+/**
+ * The navigator implementation.
+ */
+class BrowserNavigator @Inject constructor(
+ private val activity: Activity,
+ private val clipboardManager: ClipboardManager,
+ private val logger: Logger,
+ private val downloadPermissionsHelper: DownloadPermissionsHelper,
+ private val exitCleanup: ExitCleanup
+) : BrowserContract.Navigator {
+
+ override fun openSettings() {
+ activity.startActivity(Intent(activity, SettingsActivity::class.java))
+ }
+
+ override fun openReaderMode(url: String) {
+ ReadingActivity.launch(activity, url)
+ }
+
+ override fun sharePage(url: String, title: String?) {
+ IntentUtils(activity).shareUrl(url, title)
+ }
+
+ override fun copyPageLink(url: String) {
+ clipboardManager.copyToClipboard(url)
+ activity.snackbar(R.string.message_link_copied)
+ }
+
+ override fun closeBrowser() {
+ exitCleanup.cleanUp()
+ activity.finish()
+ }
+
+ override fun addToHomeScreen(url: String, title: String, favicon: Bitmap?) {
+ Utils.createShortcut(activity, url, title, favicon)
+ logger.log(TAG, "Creating shortcut: $title $url")
+ }
+
+ override fun download(pendingDownload: PendingDownload) {
+ downloadPermissionsHelper.download(
+ activity = activity,
+ url = pendingDownload.url,
+ userAgent = pendingDownload.userAgent,
+ contentDisposition = pendingDownload.contentDisposition,
+ mimeType = pendingDownload.mimeType,
+ contentLength = pendingDownload.contentLength
+ )
+ }
+
+ override fun backgroundBrowser() {
+ activity.moveTaskToBack(true)
+ }
+
+ override fun launchIncognito(url: String?) {
+ IncognitoBrowserActivity.launch(activity, url)
+ }
+
+ companion object {
+ private const val TAG = "BrowserNavigator"
+ }
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.kt b/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.kt
index ea0f7a1ae..9bede1a6e 100644
--- a/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.kt
@@ -1,321 +1,1279 @@
package acr.browser.lightning.browser
-import acr.browser.lightning.BuildConfig
-import acr.browser.lightning.R
-import acr.browser.lightning.constant.FILE
-import acr.browser.lightning.constant.INTENT_ORIGIN
-import acr.browser.lightning.constant.SCHEME_BOOKMARKS
-import acr.browser.lightning.constant.SCHEME_HOMEPAGE
-import acr.browser.lightning.di.MainScheduler
+import acr.browser.lightning.adblock.allowlist.AllowListModel
+import acr.browser.lightning.browser.data.CookieAdministrator
+import acr.browser.lightning.browser.di.Browser2Scope
+import acr.browser.lightning.browser.di.DatabaseScheduler
+import acr.browser.lightning.browser.di.DiskScheduler
+import acr.browser.lightning.browser.di.IncognitoMode
+import acr.browser.lightning.browser.di.MainScheduler
+import acr.browser.lightning.browser.download.PendingDownload
+import acr.browser.lightning.browser.history.HistoryRecord
+import acr.browser.lightning.browser.keys.KeyCombo
+import acr.browser.lightning.browser.menu.MenuSelection
+import acr.browser.lightning.browser.notification.TabCountNotifier
+import acr.browser.lightning.browser.search.SearchBoxModel
+import acr.browser.lightning.browser.tab.DownloadPageInitializer
+import acr.browser.lightning.browser.tab.HistoryPageInitializer
+import acr.browser.lightning.browser.tab.HomePageInitializer
+import acr.browser.lightning.browser.tab.NoOpInitializer
+import acr.browser.lightning.browser.tab.TabInitializer
+import acr.browser.lightning.browser.tab.TabModel
+import acr.browser.lightning.browser.tab.TabViewState
+import acr.browser.lightning.browser.tab.UrlInitializer
+import acr.browser.lightning.browser.ui.TabConfiguration
+import acr.browser.lightning.browser.ui.UiConfiguration
+import acr.browser.lightning.browser.view.targetUrl.LongPress
+import acr.browser.lightning.database.Bookmark
+import acr.browser.lightning.database.HistoryEntry
+import acr.browser.lightning.database.SearchSuggestion
+import acr.browser.lightning.database.WebPage
+import acr.browser.lightning.database.asFolder
+import acr.browser.lightning.database.bookmark.BookmarkRepository
+import acr.browser.lightning.database.downloads.DownloadEntry
+import acr.browser.lightning.database.downloads.DownloadsRepository
+import acr.browser.lightning.database.history.HistoryRepository
import acr.browser.lightning.html.bookmark.BookmarkPageFactory
-import acr.browser.lightning.html.homepage.HomePageFactory
-import acr.browser.lightning.log.Logger
-import acr.browser.lightning.preference.UserPreferences
+import acr.browser.lightning.html.history.HistoryPageFactory
+import acr.browser.lightning.search.SearchEngineProvider
import acr.browser.lightning.ssl.SslState
-import acr.browser.lightning.view.BundleInitializer
-import acr.browser.lightning.view.LightningView
-import acr.browser.lightning.view.TabInitializer
-import acr.browser.lightning.view.UrlInitializer
-import acr.browser.lightning.view.find.FindResults
-import android.app.Activity
-import android.content.Intent
-import android.webkit.URLUtil
+import acr.browser.lightning.utils.Option
+import acr.browser.lightning.utils.QUERY_PLACE_HOLDER
+import acr.browser.lightning.utils.isBookmarkUrl
+import acr.browser.lightning.utils.isDownloadsUrl
+import acr.browser.lightning.utils.isHistoryUrl
+import acr.browser.lightning.utils.isSpecialUrl
+import acr.browser.lightning.utils.smartUrlFilter
+import acr.browser.lightning.utils.value
+import androidx.activity.result.ActivityResult
+import androidx.core.net.toUri
+import io.reactivex.Maybe
import io.reactivex.Scheduler
-import io.reactivex.disposables.Disposable
+import io.reactivex.Single
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.rxkotlin.Observables
+import io.reactivex.rxkotlin.plusAssign
import io.reactivex.rxkotlin.subscribeBy
+import io.reactivex.rxkotlin.toObservable
+import javax.inject.Inject
+import kotlin.system.exitProcess
/**
- * Presenter in charge of keeping track of the current tab and setting the current tab of the
- * browser.
+ * The monolithic (oops) presenter that governs the behavior of the browser UI and interactions by
+ * the user for both default and incognito browsers. This presenter should live for the entire
+ * duration of the browser activity, which itself should not be recreated during configuration
+ * changes.
*/
-class BrowserPresenter(
- private val view: BrowserView,
- private val isIncognito: Boolean,
- private val userPreferences: UserPreferences,
- private val tabsModel: TabsManager,
+@Browser2Scope
+class BrowserPresenter @Inject constructor(
+ private val model: BrowserContract.Model,
+ private val navigator: BrowserContract.Navigator,
+ private val bookmarkRepository: BookmarkRepository,
+ private val downloadsRepository: DownloadsRepository,
+ private val historyRepository: HistoryRepository,
+ @DiskScheduler private val diskScheduler: Scheduler,
@MainScheduler private val mainScheduler: Scheduler,
- private val homePageFactory: HomePageFactory,
+ @DatabaseScheduler private val databaseScheduler: Scheduler,
+ private val historyRecord: HistoryRecord,
private val bookmarkPageFactory: BookmarkPageFactory,
- private val recentTabModel: RecentTabModel,
- private val logger: Logger
+ private val homePageInitializer: HomePageInitializer,
+ private val historyPageInitializer: HistoryPageInitializer,
+ private val downloadPageInitializer: DownloadPageInitializer,
+ private val searchBoxModel: SearchBoxModel,
+ private val searchEngineProvider: SearchEngineProvider,
+ private val uiConfiguration: UiConfiguration,
+ private val historyPageFactory: HistoryPageFactory,
+ private val allowListModel: AllowListModel,
+ private val cookieAdministrator: CookieAdministrator,
+ private val tabCountNotifier: TabCountNotifier,
+ @IncognitoMode private val incognitoMode: Boolean
) {
- private var currentTab: LightningView? = null
- private var shouldClose: Boolean = false
- private var sslStateSubscription: Disposable? = null
+ private var view: BrowserContract.View? = null
+ private var viewState: BrowserViewState = BrowserViewState(
+ displayUrl = "",
+ isRefresh = true,
+ sslState = SslState.None,
+ progress = 0,
+ enableFullMenu = true,
+ themeColor = Option.None,
+ isForwardEnabled = false,
+ isBackEnabled = false,
+ bookmarks = emptyList(),
+ isBookmarked = false,
+ isBookmarkEnabled = true,
+ isRootFolder = true,
+ findInPage = ""
+ )
+ private var tabListState: List = emptyList()
+ private var currentTab: TabModel? = null
+ private var currentFolder: Bookmark.Folder = Bookmark.Folder.Root
+ private var isTabDrawerOpen = false
+ private var isBookmarkDrawerOpen = false
+ private var isSearchViewFocused = false
+ private var tabIdOpenedFromAction = -1
+ private var pendingAction: BrowserContract.Action.LoadUrl? = null
+ private var isCustomViewShowing = false
- init {
- tabsModel.addTabNumberChangedListener(view::updateTabNumber)
+ private val compositeDisposable = CompositeDisposable()
+ private val allTabsDisposable = CompositeDisposable()
+ private var tabDisposable: CompositeDisposable = CompositeDisposable()
+
+ /**
+ * Call when the view is attached to the presenter.
+ */
+ fun onViewAttached(view: BrowserContract.View) {
+ this.view = view
+ view.updateState(viewState)
+
+ cookieAdministrator.adjustCookieSettings()
+
+ currentFolder = Bookmark.Folder.Root
+ compositeDisposable += bookmarkRepository.bookmarksAndFolders(folder = Bookmark.Folder.Root)
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribe { list ->
+ this.view?.updateState(viewState.copy(bookmarks = list, isRootFolder = true))
+ }
+
+ compositeDisposable += model.tabsListChanges()
+ .observeOn(mainScheduler)
+ .subscribe { list ->
+ this.view?.updateTabs(list.map { it.asViewState() })
+
+ allTabsDisposable.clear()
+ list.subscribeToUpdates(allTabsDisposable)
+
+ tabCountNotifier.notifyTabCountChange(list.size)
+ }
+
+ compositeDisposable += model.initializeTabs()
+ .observeOn(mainScheduler)
+ .switchIfEmpty(model.createTab(homePageInitializer).map(::listOf))
+ .subscribe { list ->
+ selectTab(model.selectTab(list.last().id))
+ }
}
/**
- * Initializes the tab manager with the new intent that is handed in by the BrowserActivity.
- *
- * @param intent the intent to handle, may be null.
- */
- fun setupTabs(intent: Intent?) {
- tabsModel.initializeTabs(view as Activity, intent, isIncognito)
- .subscribeBy(
- onSuccess = {
- // At this point we always have at least a tab in the tab manager
- view.notifyTabViewInitialized()
- view.updateTabNumber(tabsModel.size())
- tabChanged(tabsModel.positionOf(it))
- }
- )
+ * Call when the view is detached from the presenter.
+ */
+ fun onViewDetached() {
+ view = null
+
+ compositeDisposable.dispose()
+ tabDisposable.dispose()
}
/**
- * Notify the presenter that a change occurred to the current tab. Currently doesn't do anything
- * other than tell the view to notify the adapter about the change.
- *
- * @param tab the tab that changed, may be null.
+ * Call when the view is hidden (i.e. the browser is sent to the background).
*/
- fun tabChangeOccurred(tab: LightningView?) = tab?.let {
- view.notifyTabViewChanged(tabsModel.indexOfTab(it))
+ fun onViewHidden() {
+ model.freeze()
+ tabIdOpenedFromAction = -1
}
- private fun onTabChanged(newTab: LightningView?) {
- logger.log(TAG, "On tab changed")
- view.updateSslState(newTab?.currentSslState() ?: SslState.None)
+ private fun TabModel.asViewState(): TabViewState = TabViewState(
+ id = id,
+ icon = favicon,
+ title = title,
+ isSelected = isForeground
+ )
+
+ private fun List.updateId(
+ id: Int,
+ map: (TabViewState) -> TabViewState
+ ): List = map {
+ if (it.id == id) {
+ map(it)
+ } else {
+ it
+ }
+ }
+
+ private fun selectTab(tabModel: TabModel?) {
+ if (currentTab == tabModel) {
+ return
+ }
+ currentTab?.isForeground = false
+ currentTab = tabModel
+ currentTab?.isForeground = true
+
+ view?.clearSearchFocus()
+
+ val tab = tabModel ?: return run {
+ view.updateState(
+ viewState.copy(
+ displayUrl = searchBoxModel.getDisplayContent(
+ url = "",
+ title = null,
+ isLoading = false
+ ),
+ enableFullMenu = false,
+ isForwardEnabled = false,
+ isBackEnabled = false,
+ sslState = SslState.None,
+ progress = 100,
+ findInPage = ""
+ )
+ )
+ view.updateTabs(tabListState.map { it.copy(isSelected = false) })
+ }
+
+ view?.showToolbar()
+ view?.closeTabDrawer()
+
+ view.updateTabs(tabListState.map { it.copy(isSelected = it.id == tab.id) })
+
+ tabDisposable.dispose()
+ tabDisposable = CompositeDisposable()
+ tabDisposable += Observables.combineLatest(
+ tab.sslChanges().startWith(tab.sslState),
+ tab.titleChanges().startWith(tab.title),
+ tab.urlChanges().startWith(tab.url),
+ tab.loadingProgress().startWith(tab.loadingProgress),
+ tab.canGoBackChanges().startWith(tab.canGoBack()),
+ tab.canGoForwardChanges().startWith(tab.canGoForward()),
+ tab.urlChanges().startWith(tab.url).observeOn(diskScheduler)
+ .flatMapSingle(bookmarkRepository::isBookmark).observeOn(mainScheduler),
+ tab.urlChanges().startWith(tab.url).map(String::isSpecialUrl),
+ tab.themeColorChanges().startWith(tab.themeColor)
+ ) { sslState, title, url, progress, canGoBack, canGoForward, isBookmark, isSpecialUrl, themeColor ->
+ viewState.copy(
+ displayUrl = searchBoxModel.getDisplayContent(
+ url = url,
+ title = title,
+ isLoading = progress < 100
+ ).takeIf { !isSearchViewFocused } ?: viewState.displayUrl,
+ enableFullMenu = !url.isSpecialUrl(),
+ themeColor = Option.Some(themeColor),
+ isRefresh = (progress == 100).takeIf { !isSearchViewFocused }
+ ?: viewState.isRefresh,
+ isForwardEnabled = canGoForward,
+ isBackEnabled = canGoBack,
+ sslState = sslState.takeIf { !isSearchViewFocused } ?: viewState.sslState,
+ progress = progress,
+ isBookmarked = isBookmark,
+ isBookmarkEnabled = !isSpecialUrl,
+ findInPage = tab.findQuery.orEmpty()
+ )
+ }.observeOn(mainScheduler)
+ .subscribe { view.updateState(it) }
+
+ tabDisposable += tab.downloadRequests()
+ .subscribeOn(mainScheduler)
+ .subscribeBy(onNext = navigator::download)
+
+ tabDisposable += tab.urlChanges()
+ .distinctUntilChanged()
+ .subscribeOn(mainScheduler)
+ .subscribeBy { view?.showToolbar() }
- sslStateSubscription?.dispose()
- sslStateSubscription = newTab
- ?.sslStateObservable()
- ?.observeOn(mainScheduler)
- ?.subscribe(view::updateSslState)
+ tabDisposable += tab.createWindowRequests()
+ .subscribeOn(mainScheduler)
+ .subscribeBy { createNewTabAndSelect(it, shouldSelect = true) }
- val webView = newTab?.webView
+ tabDisposable += tab.closeWindowRequests()
+ .subscribeOn(mainScheduler)
+ .subscribeBy { onTabClose(tabListState.indexOfCurrentTab()) }
- if (newTab == null) {
- view.removeTabView()
- currentTab?.let {
- it.pauseTimers()
- it.onDestroy()
+ tabDisposable += tab.fileChooserRequests()
+ .subscribeOn(mainScheduler)
+ .subscribeBy { view?.showFileChooser(it) }
+
+ tabDisposable += tab.showCustomViewRequests()
+ .subscribeOn(mainScheduler)
+ .subscribeBy {
+ view?.showCustomView(it)
+ isCustomViewShowing = true
}
- } else {
- if (webView == null) {
- view.removeTabView()
- currentTab?.let {
- it.pauseTimers()
- it.onDestroy()
+
+ tabDisposable += tab.hideCustomViewRequests()
+ .subscribeOn(mainScheduler)
+ .subscribeBy {
+ view?.hideCustomView()
+ isCustomViewShowing = false
+ }
+ }
+
+ private fun List.subscribeToUpdates(compositeDisposable: CompositeDisposable) {
+ forEach { tabModel ->
+ compositeDisposable += Observables.combineLatest(
+ tabModel.titleChanges().startWith(tabModel.title),
+ tabModel.faviconChanges()
+ .startWith(Option.fromNullable(tabModel.favicon))
+ ).distinctUntilChanged()
+ .subscribeOn(mainScheduler)
+ .subscribeBy { (title, bitmap) ->
+ view.updateTabs(tabListState.updateId(tabModel.id) {
+ it.copy(title = title, icon = bitmap.value())
+ })
+
+ tabModel.url.takeIf { !it.isSpecialUrl() && it.isNotBlank() }?.let {
+ historyRecord.recordVisit(title, it)
+ }
}
+ }
+ }
+
+ /**
+ * Call when a new action is triggered, such as the user opening a new URL in the browser.
+ */
+ fun onNewAction(action: BrowserContract.Action) {
+ when (action) {
+ is BrowserContract.Action.LoadUrl -> if (action.url.isSpecialUrl()) {
+ view?.showLocalFileBlockedDialog()
+ pendingAction = action
} else {
- currentTab.let {
- // TODO: Restore this when Google fixes the bug where the WebView is
- // blank after calling onPause followed by onResume.
- // currentTab.onPause();
- it?.isForegroundTab = false
- }
+ createNewTabAndSelect(
+ tabInitializer = UrlInitializer(action.url),
+ shouldSelect = true,
+ markAsOpenedFromAction = true
+ )
+ }
+ BrowserContract.Action.Panic -> panicClean()
+ }
+ }
+
+ /**
+ * Call when the user confirms that they do or do not want to allow a local file to be opened
+ * in the browser. This is a security gate to prevent malicious local files from being opened
+ * in the browser without the user's knowledge.
+ */
+ fun onConfirmOpenLocalFile(allow: Boolean) {
+ if (allow) {
+ pendingAction?.let {
+ createNewTabAndSelect(
+ tabInitializer = UrlInitializer(it.url),
+ shouldSelect = true,
+ markAsOpenedFromAction = true
+ )
+ }
+ }
+ pendingAction = null
+ }
+
+ private fun panicClean() {
+ createNewTabAndSelect(tabInitializer = NoOpInitializer(), shouldSelect = true)
+ model.clean()
+
+ historyPageFactory.deleteHistoryPage().subscribe()
+ model.deleteAllTabs().subscribe()
+ navigator.closeBrowser()
+
+ // System exit needed in the case of receiving
+ // the panic intent since finish() isn't completely
+ // closing the browser
+ exitProcess(1)
+ }
+
+ /**
+ * Call when the user selects an option from the menu.
+ */
+ fun onMenuClick(menuSelection: MenuSelection) {
+ when (menuSelection) {
+ MenuSelection.NEW_TAB -> onNewTabClick()
+ MenuSelection.NEW_INCOGNITO_TAB -> navigator.launchIncognito(url = null)
+ MenuSelection.SHARE -> currentTab?.url?.takeIf { !it.isSpecialUrl() }?.let {
+ navigator.sharePage(url = it, title = currentTab?.title)
+ }
+ MenuSelection.HISTORY -> createNewTabAndSelect(
+ historyPageInitializer,
+ shouldSelect = true
+ )
+ MenuSelection.DOWNLOADS -> createNewTabAndSelect(
+ downloadPageInitializer,
+ shouldSelect = true
+ )
+ MenuSelection.FIND -> view?.showFindInPageDialog()
+ MenuSelection.COPY_LINK -> currentTab?.url?.takeIf { !it.isSpecialUrl() }
+ ?.let(navigator::copyPageLink)
+ MenuSelection.ADD_TO_HOME -> currentTab?.url?.takeIf { !it.isSpecialUrl() }
+ ?.let { addToHomeScreen() }
+ MenuSelection.BOOKMARKS -> view?.openBookmarkDrawer()
+ MenuSelection.ADD_BOOKMARK -> currentTab?.url?.takeIf { !it.isSpecialUrl() }
+ ?.let { showAddBookmarkDialog() }
+ MenuSelection.READER -> currentTab?.url?.takeIf { !it.isSpecialUrl() }
+ ?.let(navigator::openReaderMode)
+ MenuSelection.SETTINGS -> navigator.openSettings()
+ MenuSelection.BACK -> onBackClick()
+ MenuSelection.FORWARD -> onForwardClick()
+ }
+ }
- newTab.resumeTimers()
- newTab.onResume()
- newTab.isForegroundTab = true
-
- view.updateProgress(newTab.progress)
- view.setBackButtonEnabled(newTab.canGoBack())
- view.setForwardButtonEnabled(newTab.canGoForward())
- view.updateUrl(newTab.url, false)
- view.setTabView(webView)
- val index = tabsModel.indexOfTab(newTab)
- if (index >= 0) {
- view.notifyTabViewChanged(tabsModel.indexOfTab(newTab))
+ private fun addToHomeScreen() {
+ currentTab?.let {
+ navigator.addToHomeScreen(it.url, it.title, it.favicon)
+ }
+ }
+
+ private fun createNewTabAndSelect(
+ tabInitializer: TabInitializer,
+ shouldSelect: Boolean,
+ markAsOpenedFromAction: Boolean = false
+ ) {
+ compositeDisposable += model.createTab(tabInitializer)
+ .observeOn(mainScheduler)
+ .subscribe { tab ->
+ if (shouldSelect) {
+ selectTab(model.selectTab(tab.id))
+ if (markAsOpenedFromAction) {
+ tabIdOpenedFromAction = tab.id
+ }
}
}
+ }
+
+ private fun List.tabIndexForId(id: Int?): Int =
+ indexOfFirst { it.id == id }
+
+ private fun List.indexOfCurrentTab(): Int = tabIndexForId(currentTab?.id)
+
+ /**
+ * Call when the user selects a combination of keys to perform a shortcut.
+ */
+ fun onKeyComboClick(keyCombo: KeyCombo) {
+ when (keyCombo) {
+ KeyCombo.CTRL_F -> view?.showFindInPageDialog()
+ KeyCombo.CTRL_T -> onNewTabClick()
+ KeyCombo.CTRL_W -> onTabClose(tabListState.indexOfCurrentTab())
+ KeyCombo.CTRL_Q -> view?.showCloseBrowserDialog(tabListState.indexOfCurrentTab())
+ KeyCombo.CTRL_R -> onRefreshOrStopClick()
+ KeyCombo.CTRL_TAB -> TODO()
+ KeyCombo.CTRL_SHIFT_TAB -> TODO()
+ KeyCombo.SEARCH -> TODO()
+ KeyCombo.ALT_0 -> onTabClick(0.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_1 -> onTabClick(1.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_2 -> onTabClick(2.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_3 -> onTabClick(3.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_4 -> onTabClick(4.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_5 -> onTabClick(5.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_6 -> onTabClick(6.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_7 -> onTabClick(7.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_8 -> onTabClick(8.coerceAtMost(tabListState.size - 1))
+ KeyCombo.ALT_9 -> onTabClick(9.coerceAtMost(tabListState.size - 1))
}
+ }
- currentTab = newTab
+ /**
+ * Call when the user selects a tab to switch to at the provided [index].
+ */
+ fun onTabClick(index: Int) {
+ selectTab(model.selectTab(tabListState[index].id))
}
/**
- * Closes all tabs but the current tab.
+ * Call when the user long presses on a tab at the provided [index].
*/
- fun closeAllOtherTabs() {
+ fun onTabLongClick(index: Int) {
+ view?.showCloseBrowserDialog(tabListState[index].id)
+ }
- while (tabsModel.last() != tabsModel.indexOfCurrentTab()) {
- deleteTab(tabsModel.last())
+ private fun List.nextSelected(removedIndex: Int): T? {
+ val nextIndex = when {
+ removedIndex > 0 -> removedIndex - 1
+ size > removedIndex + 1 -> removedIndex + 1
+ else -> -1
}
+ return if (nextIndex >= 0) {
+ this[nextIndex]
+ } else {
+ null
+ }
+ }
- while (0 != tabsModel.indexOfCurrentTab()) {
- deleteTab(0)
+ /**
+ * Call when the user clicks on the close button for the tab at the provided [index]
+ */
+ fun onTabClose(index: Int) {
+ if (index == -1) {
+ // If the user clicks on close multiple times, the index may be -1 if the view is in the
+ // process of being removed.
+ return
}
+ val nextTab = tabListState.nextSelected(index)
+
+ val currentTabId = currentTab?.id
+ val needToSelectNextTab = tabListState[index].id == currentTabId
+ compositeDisposable += model.deleteTab(tabListState[index].id)
+ .observeOn(mainScheduler)
+ .subscribe {
+ if (needToSelectNextTab) {
+ nextTab?.id?.let {
+ selectTab(model.selectTab(it))
+ if (tabIdOpenedFromAction == currentTabId) {
+ tabIdOpenedFromAction = -1
+ navigator.backgroundBrowser()
+ }
+ } ?: run {
+ selectTab(tabModel = null)
+ navigator.closeBrowser()
+ }
+
+ }
+ }
}
- private fun mapHomepageToCurrentUrl(): String = when (val homepage = userPreferences.homepage) {
- SCHEME_HOMEPAGE -> "$FILE${homePageFactory.createHomePage()}"
- SCHEME_BOOKMARKS -> "$FILE${bookmarkPageFactory.createBookmarkPage(null)}"
- else -> homepage
+ /**
+ * Call when the tab drawer is opened or closed.
+ *
+ * @param isOpen True if the drawer is now open, false if it is now closed.
+ */
+ fun onTabDrawerMoved(isOpen: Boolean) {
+ isTabDrawerOpen = isOpen
}
/**
- * Deletes the tab at the specified position.
+ * Call when the bookmark drawer is opened or closed.
*
- * @param position the position at which to delete the tab.
- */
- fun deleteTab(position: Int) {
- logger.log(TAG, "deleting tab...")
- val tabToDelete = tabsModel.getTabAtPosition(position) ?: return
-
- recentTabModel.addClosedTab(tabToDelete.saveState())
-
- val isShown = tabToDelete.isShown
- val shouldClose = shouldClose && isShown && tabToDelete.isNewTab
- val currentTab = tabsModel.currentTab
- if (tabsModel.size() == 1
- && currentTab != null
- && URLUtil.isFileUrl(currentTab.url)
- && currentTab.url == mapHomepageToCurrentUrl()) {
- view.closeActivity()
- return
- } else {
- if (isShown) {
- view.removeTabView()
+ * @param isOpen True if the drawer is now open, false if it is now closed.
+ */
+ fun onBookmarkDrawerMoved(isOpen: Boolean) {
+ isBookmarkDrawerOpen = isOpen
+ }
+
+ /**
+ * Called when the user clicks on the device back button or swipes to go back. Differentiated
+ * from [onBackClick] which is called when the user presses the browser's back button.
+ */
+ fun onNavigateBack() {
+ when {
+ isCustomViewShowing -> {
+ view?.hideCustomView()
+ currentTab?.hideCustomView()
+ }
+ isTabDrawerOpen -> view?.closeTabDrawer()
+ isBookmarkDrawerOpen -> if (currentFolder != Bookmark.Folder.Root) {
+ onBookmarkMenuClick()
+ } else {
+ view?.closeBookmarkDrawer()
}
- val currentDeleted = tabsModel.deleteTab(position)
- if (currentDeleted) {
- tabChanged(tabsModel.indexOfCurrentTab())
+ currentTab?.canGoBack() == true -> currentTab?.goBack()
+ currentTab?.canGoBack() == false -> if (incognitoMode) {
+ currentTab?.id?.let {
+ view?.showCloseBrowserDialog(it)
+ }
+ } else if (tabIdOpenedFromAction == currentTab?.id) {
+ onTabClose(tabListState.indexOfCurrentTab())
+ } else {
+ navigator.backgroundBrowser()
}
}
+ }
- val afterTab = tabsModel.currentTab
- view.notifyTabViewRemoved(position)
-
- if (afterTab == null) {
- view.closeBrowser()
- return
- } else if (afterTab !== currentTab) {
- view.notifyTabViewChanged(tabsModel.indexOfCurrentTab())
+ /**
+ * Called when the user presses the browser's back button.
+ */
+ fun onBackClick() {
+ if (currentTab?.canGoBack() == true) {
+ currentTab?.goBack()
}
+ }
- if (shouldClose && !isIncognito) {
- this.shouldClose = false
- view.closeActivity()
+ /**
+ * Called when the user presses the browser's forward button.
+ */
+ fun onForwardClick() {
+ if (currentTab?.canGoForward() == true) {
+ currentTab?.goForward()
}
+ }
+
+ /**
+ * Call when the user clicks on the home button.
+ */
+ fun onHomeClick() {
+ currentTab?.loadFromInitializer(homePageInitializer)
+ }
+
+ /**
+ * Call when the user clicks on the open new tab button.
+ */
+ fun onNewTabClick() {
+ createNewTabAndSelect(homePageInitializer, shouldSelect = true)
+ }
- view.updateTabNumber(tabsModel.size())
+ /**
+ * Call when the user long clicks on the new tab button, indicating that they want to re-open
+ * the last closed tab.
+ */
+ fun onNewTabLongClick() {
+ compositeDisposable += model.reopenTab()
+ .observeOn(mainScheduler)
+ .subscribeBy { tab ->
+ selectTab(model.selectTab(tab.id))
+ }
+ }
- logger.log(TAG, "...deleted tab")
+ /**
+ * Call when the user clicks on the refresh (or stop/delete) button that is located in the
+ * search bar.
+ */
+ fun onRefreshOrStopClick() {
+ if (isSearchViewFocused) {
+ view?.renderState(viewState.copy(displayUrl = ""))
+ return
+ }
+ if (currentTab?.loadingProgress != 100) {
+ currentTab?.stopLoading()
+ } else {
+ reload()
+ }
+ }
+
+ private fun reload() {
+ val currentUrl = currentTab?.url
+ if (currentUrl?.isSpecialUrl() == true) {
+ when {
+ currentUrl.isBookmarkUrl() ->
+ compositeDisposable += bookmarkPageFactory.buildPage()
+ .subscribeOn(diskScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy {
+ currentTab?.reload()
+ }
+ currentUrl.isDownloadsUrl() ->
+ currentTab?.loadFromInitializer(downloadPageInitializer)
+ currentUrl.isHistoryUrl() ->
+ currentTab?.loadFromInitializer(historyPageInitializer)
+ else -> currentTab?.reload()
+ }
+ } else {
+ currentTab?.reload()
+ }
}
/**
- * Handle a new intent from the the main BrowserActivity.
+ * Call when the focus state changes for the search bar.
*
- * @param intent the intent to handle, may be null.
+ * @param isFocused True if the view is now focused, false otherwise.
*/
- fun onNewIntent(intent: Intent?) = tabsModel.doAfterInitialization {
- val url = if (intent?.action == Intent.ACTION_WEB_SEARCH) {
- tabsModel.extractSearchFromIntent(intent)
+ fun onSearchFocusChanged(isFocused: Boolean) {
+ isSearchViewFocused = isFocused
+ if (isFocused) {
+ view?.updateState(
+ viewState.copy(
+ sslState = SslState.None,
+ isRefresh = false,
+ displayUrl = currentTab?.url?.takeIf { !it.isSpecialUrl() }.orEmpty()
+ )
+ )
} else {
- intent?.dataString
+ view?.updateState(
+ viewState.copy(
+ sslState = currentTab?.sslState ?: SslState.None,
+ isRefresh = (currentTab?.loadingProgress ?: 0) == 100,
+ displayUrl = searchBoxModel.getDisplayContent(
+ url = currentTab?.url.orEmpty(),
+ title = currentTab?.title.orEmpty(),
+ isLoading = (currentTab?.loadingProgress ?: 0) < 100
+ )
+ )
+ )
+ }
+ }
+
+ /**
+ * Call when the user submits a search [query] to the search bar. At this point the user has
+ * provided intent to search and is no longer trying to manipulate the query.
+ */
+ fun onSearch(query: String) {
+ if (query.isEmpty()) {
+ return
}
+ currentTab?.stopLoading()
+ val searchUrl = searchEngineProvider.provideSearchEngine().queryUrl + QUERY_PLACE_HOLDER
+ val url = smartUrlFilter(query.trim(), true, searchUrl)
+ view?.updateState(
+ viewState.copy(
+ displayUrl = searchBoxModel.getDisplayContent(
+ url = url,
+ title = currentTab?.title,
+ isLoading = (currentTab?.loadingProgress ?: 0) < 100
+ )
+ )
+ )
+ currentTab?.loadUrl(url)
+ }
- val tabHashCode = intent?.extras?.getInt(INTENT_ORIGIN, 0) ?: 0
+ /**
+ * Call when the user enters a [query] to look for in the current web page.
+ */
+ fun onFindInPage(query: String) {
+ currentTab?.find(query)
+ view?.updateState(viewState.copy(findInPage = query))
+ }
- if (tabHashCode != 0 && url != null) {
- tabsModel.getTabForHashCode(tabHashCode)?.loadUrl(url)
- } else if (url != null) {
- if (URLUtil.isFileUrl(url)) {
- view.showBlockedLocalFileDialog {
- newTab(UrlInitializer(url), true)
- shouldClose = true
- tabsModel.lastTab()?.isNewTab = true
- }
- } else {
- newTab(UrlInitializer(url), true)
- shouldClose = true
- tabsModel.lastTab()?.isNewTab = true
+ /**
+ * Call when the user selects to move to the next highlighted word in the web page.
+ */
+ fun onFindNext() {
+ currentTab?.findNext()
+ }
+
+ /**
+ * Call when the user selects to move to the previous highlighted word in the web page.
+ */
+ fun onFindPrevious() {
+ currentTab?.findPrevious()
+ }
+
+ /**
+ * Call when the user chooses to dismiss the find in page UI component.
+ */
+ fun onFindDismiss() {
+ currentTab?.clearFindMatches()
+ view?.updateState(viewState.copy(findInPage = ""))
+ }
+
+ /**
+ * Call when the user selects a search suggestion that was suggested by the search box.
+ */
+ fun onSearchSuggestionClicked(webPage: WebPage) {
+ val url = when (webPage) {
+ is HistoryEntry,
+ is Bookmark.Entry -> webPage.url
+ is SearchSuggestion -> webPage.title
+ else -> null
+ } ?: error("Other types cannot be search suggestions: $webPage")
+
+ onSearch(url)
+ }
+
+ /**
+ * Call when the user clicks on the SSL icon in the search box.
+ */
+ fun onSslIconClick() {
+ currentTab?.sslCertificateInfo?.let {
+ view?.showSslDialog(it)
+ }
+ }
+
+ /**
+ * Call when the user clicks on a bookmark from the bookmark list at the provided [index].
+ */
+ fun onBookmarkClick(index: Int) {
+ when (val bookmark = viewState.bookmarks[index]) {
+ is Bookmark.Entry -> {
+ currentTab?.loadUrl(bookmark.url)
+ view?.closeBookmarkDrawer()
+ }
+ Bookmark.Folder.Root -> error("Cannot click on root folder")
+ is Bookmark.Folder.Entry -> {
+ currentFolder = bookmark
+ compositeDisposable += bookmarkRepository
+ .bookmarksAndFolders(folder = bookmark)
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribe { list ->
+ view?.updateState(viewState.copy(bookmarks = list, isRootFolder = false))
+ }
}
}
}
+ private fun BookmarkRepository.bookmarksAndFolders(folder: Bookmark.Folder): Single> =
+ getBookmarksFromFolderSorted(folder = folder.title)
+ .concatWith(Single.defer {
+ if (folder == Bookmark.Folder.Root) {
+ getFoldersSorted()
+ } else {
+ Single.just(emptyList())
+ }
+ })
+ .toList()
+ .map(MutableList>::flatten)
+
/**
- * Call when the user long presses the new tab button.
+ * Call when the user long presses on a bookmark in the bookmark list at the provided [index].
*/
- fun onNewTabLongClicked() {
- recentTabModel.lastClosed()?.let {
- newTab(BundleInitializer(it), true)
- view.showSnackbar(R.string.reopening_recent_tab)
+ fun onBookmarkLongClick(index: Int) {
+ when (val item = viewState.bookmarks[index]) {
+ is Bookmark.Entry -> view?.showBookmarkOptionsDialog(item)
+ is Bookmark.Folder.Entry -> view?.showFolderOptionsDialog(item)
+ Bookmark.Folder.Root -> Unit // Root is not clickable
}
}
/**
- * Loads a URL in the current tab.
- *
- * @param url the URL to load, must not be null.
+ * Call when the user clicks on the page tools button.
*/
- fun loadUrlInCurrentView(url: String) {
- tabsModel.currentTab?.loadUrl(url)
+ fun onToolsClick() {
+ val currentUrl = currentTab?.url ?: return
+ view?.showToolsDialog(
+ areAdsAllowed = allowListModel.isUrlAllowedAds(currentUrl),
+ shouldShowAdBlockOption = !currentUrl.isSpecialUrl()
+ )
}
/**
- * Notifies the presenter that it should shut down. This should be called when the
- * BrowserActivity is destroyed so that we don't leak any memory.
+ * Call when the user chooses to toggle the desktop user agent on/off.
*/
- fun shutdown() {
- onTabChanged(null)
- tabsModel.cancelPendingWork()
- sslStateSubscription?.dispose()
+ fun onToggleDesktopAgent() {
+ currentTab?.toggleDesktopAgent()
+ currentTab?.reload()
}
/**
- * Notifies the presenter that we wish to switch to a different tab at the specified position.
- * If the position is not in the model, this method will do nothing.
- *
- * @param position the position of the tab to switch to.
+ * Call when the user chooses to toggle ad blocking on/off for the current web page.
+ */
+ fun onToggleAdBlocking() {
+ val currentUrl = currentTab?.url ?: return
+ if (allowListModel.isUrlAllowedAds(currentUrl)) {
+ allowListModel.removeUrlFromAllowList(currentUrl)
+ } else {
+ allowListModel.addUrlToAllowList(currentUrl)
+ }
+ currentTab?.reload()
+ }
+
+ /**
+ * Call when the user clicks on the star icon to add a bookmark for the current page or remove
+ * the existing one.
*/
- fun tabChanged(position: Int) {
- if (position < 0 || position >= tabsModel.size()) {
- logger.log(TAG, "tabChanged invalid position: $position")
+ fun onStarClick() {
+ val url = currentTab?.url ?: return
+ val title = currentTab?.title.orEmpty()
+ if (url.isSpecialUrl()) {
return
}
+ compositeDisposable += bookmarkRepository.isBookmark(url)
+ .flatMapMaybe {
+ if (it) {
+ bookmarkRepository.deleteBookmark(
+ Bookmark.Entry(
+ url = url,
+ title = title,
+ position = 0,
+ folder = Bookmark.Folder.Root
+ )
+ ).toMaybe()
+ } else {
+ Maybe.empty()
+ }
+ }
+ .doOnComplete(::showAddBookmarkDialog)
+ .flatMapSingleElement { bookmarkRepository.bookmarksAndFolders(folder = currentFolder) }
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy { list ->
+ this.view?.updateState(viewState.copy(bookmarks = list))
+ }
+ }
- logger.log(TAG, "tabChanged: $position")
- onTabChanged(tabsModel.switchToTab(position))
+ private fun showAddBookmarkDialog() {
+ compositeDisposable += bookmarkRepository.getFolderNames()
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy {
+ view?.showAddBookmarkDialog(
+ title = currentTab?.title.orEmpty(),
+ url = currentTab?.url.orEmpty(),
+ folders = it
+ )
+ }
}
/**
- * Open a new tab with the specified URL. You can choose to show the tab or load it in the
- * background.
+ * Call when the user confirms the details for adding a bookmark.
*
- * @param tabInitializer the tab initializer to run after the tab as been created.
- * @param show whether or not to switch to this tab after opening it.
- * @return true if we successfully created the tab, false if we have hit max tabs.
+ * @param title The title of the bookmark.
+ * @param url The URL of the bookmark.
+ * @param folder The name of the folder the bookmark is in.
*/
- fun newTab(tabInitializer: TabInitializer, show: Boolean): Boolean {
- // Limit number of tabs for limited version of app
- if (!BuildConfig.FULL_VERSION && tabsModel.size() >= 10) {
- view.showSnackbar(R.string.max_tabs)
- return false
+ fun onBookmarkConfirmed(title: String, url: String, folder: String) {
+ compositeDisposable += bookmarkRepository.addBookmarkIfNotExists(
+ Bookmark.Entry(
+ url = url,
+ title = title,
+ position = 0,
+ folder = folder.asFolder()
+ )
+ ).flatMap { bookmarkRepository.bookmarksAndFolders(folder = currentFolder) }
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy { list ->
+ this.view?.updateState(viewState.copy(bookmarks = list))
+ }
+ }
+
+ /**
+ * Call when the user confirms the details when editing a bookmark.
+ *
+ * @param title The title of the bookmark.
+ * @param url The URL of the bookmark.
+ * @param folder The name of the folder the bookmark is in.
+ */
+ fun onBookmarkEditConfirmed(title: String, url: String, folder: String) {
+ compositeDisposable += bookmarkRepository.editBookmark(
+ oldBookmark = Bookmark.Entry(
+ url = url,
+ title = "",
+ position = 0,
+ folder = Bookmark.Folder.Root
+ ),
+ newBookmark = Bookmark.Entry(
+ url = url,
+ title = title,
+ position = 0,
+ folder = folder.asFolder()
+ )
+ ).andThen(bookmarkRepository.bookmarksAndFolders(folder = currentFolder))
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy { list ->
+ this.view?.updateState(viewState.copy(bookmarks = list))
+ if (currentTab?.url?.isBookmarkUrl() == true) {
+ reload()
+ }
+ }
+ }
+
+ /**
+ * Call when the user confirms a name change to an existing folder.
+ *
+ * @param oldTitle The previous title of the folder.
+ * @param newTitle The new title of the folder.
+ */
+ fun onBookmarkFolderRenameConfirmed(oldTitle: String, newTitle: String) {
+ compositeDisposable += bookmarkRepository.renameFolder(oldTitle, newTitle)
+ .andThen(bookmarkRepository.bookmarksAndFolders(folder = currentFolder))
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribe { list ->
+ this.view?.updateState(viewState.copy(bookmarks = list))
+ if (currentTab?.url?.isBookmarkUrl() == true) {
+ reload()
+ }
+ }
+ }
+
+ /**
+ * Call when the user clicks on a menu [option] for the provided [bookmark].
+ */
+ fun onBookmarkOptionClick(
+ bookmark: Bookmark.Entry,
+ option: BrowserContract.BookmarkOptionEvent
+ ) {
+ when (option) {
+ BrowserContract.BookmarkOptionEvent.NEW_TAB ->
+ createNewTabAndSelect(UrlInitializer(bookmark.url), shouldSelect = true)
+ BrowserContract.BookmarkOptionEvent.BACKGROUND_TAB ->
+ createNewTabAndSelect(UrlInitializer(bookmark.url), shouldSelect = false)
+ BrowserContract.BookmarkOptionEvent.INCOGNITO_TAB -> navigator.launchIncognito(bookmark.url)
+ BrowserContract.BookmarkOptionEvent.SHARE ->
+ navigator.sharePage(url = bookmark.url, title = bookmark.title)
+ BrowserContract.BookmarkOptionEvent.COPY_LINK ->
+ navigator.copyPageLink(bookmark.url)
+ BrowserContract.BookmarkOptionEvent.REMOVE ->
+ compositeDisposable += bookmarkRepository.deleteBookmark(bookmark)
+ .flatMap { bookmarkRepository.bookmarksAndFolders(folder = currentFolder) }
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribe { list ->
+ view?.updateState(viewState.copy(bookmarks = list))
+ if (currentTab?.url?.isBookmarkUrl() == true) {
+ reload()
+ }
+ }
+ BrowserContract.BookmarkOptionEvent.EDIT ->
+ compositeDisposable += bookmarkRepository.getFolderNames()
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy { folders ->
+ view?.showEditBookmarkDialog(
+ bookmark.title,
+ bookmark.url,
+ bookmark.folder.title,
+ folders
+ )
+ }
+ }
+ }
+
+ /**
+ * Call when the user clicks on a menu [option] for the provided [folder].
+ */
+ fun onFolderOptionClick(folder: Bookmark.Folder, option: BrowserContract.FolderOptionEvent) {
+ when (option) {
+ BrowserContract.FolderOptionEvent.RENAME -> view?.showEditFolderDialog(folder.title)
+ BrowserContract.FolderOptionEvent.REMOVE ->
+ compositeDisposable += bookmarkRepository.deleteFolder(folder.title)
+ .andThen(bookmarkRepository.bookmarksAndFolders(folder = currentFolder))
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribe { list ->
+ view?.updateState(viewState.copy(bookmarks = list))
+ if (currentTab?.url?.isBookmarkUrl() == true) {
+ reload()
+ currentTab?.goBack()
+ }
+ }
+ }
+ }
+
+ /**
+ * Call when the user clicks on a menu [option] for the provided [download] entry.
+ */
+ fun onDownloadOptionClick(
+ download: DownloadEntry,
+ option: BrowserContract.DownloadOptionEvent
+ ) {
+ when (option) {
+ BrowserContract.DownloadOptionEvent.DELETE ->
+ compositeDisposable += downloadsRepository.deleteAllDownloads()
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy {
+ if (currentTab?.url?.isDownloadsUrl() == true) {
+ reload()
+ }
+ }
+ BrowserContract.DownloadOptionEvent.DELETE_ALL ->
+ compositeDisposable += downloadsRepository.deleteDownload(download.url)
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy {
+ if (currentTab?.url?.isDownloadsUrl() == true) {
+ reload()
+ }
+ }
}
+ }
+
+ /**
+ * Call when the user clicks on a menu [option] for the provided [historyEntry].
+ */
+ fun onHistoryOptionClick(
+ historyEntry: HistoryEntry,
+ option: BrowserContract.HistoryOptionEvent
+ ) {
+ when (option) {
+ BrowserContract.HistoryOptionEvent.NEW_TAB ->
+ createNewTabAndSelect(UrlInitializer(historyEntry.url), shouldSelect = true)
+ BrowserContract.HistoryOptionEvent.BACKGROUND_TAB ->
+ createNewTabAndSelect(UrlInitializer(historyEntry.url), shouldSelect = false)
+ BrowserContract.HistoryOptionEvent.INCOGNITO_TAB ->
+ navigator.launchIncognito(historyEntry.url)
+ BrowserContract.HistoryOptionEvent.SHARE ->
+ navigator.sharePage(url = historyEntry.url, title = historyEntry.title)
+ BrowserContract.HistoryOptionEvent.COPY_LINK -> navigator.copyPageLink(historyEntry.url)
+ BrowserContract.HistoryOptionEvent.REMOVE ->
+ compositeDisposable += historyRepository.deleteHistoryEntry(historyEntry.url)
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy {
+ if (currentTab?.url?.isHistoryUrl() == true) {
+ reload()
+ }
+ }
+ }
+ }
+
+ /**
+ * Call when the user clicks on the button to open reading mode..
+ */
+ fun onReadingModeClick() {
+ currentTab?.url?.takeIf { !it.isSpecialUrl() }
+ ?.let(navigator::openReaderMode)
+ }
+
+ /**
+ * Call when the user clicks on the tab count button (or home button in desktop mode, or
+ * incognito icon in incognito mode).
+ */
+ fun onTabCountViewClick() {
+ if (uiConfiguration.tabConfiguration == TabConfiguration.DRAWER) {
+ view?.openTabDrawer()
+ } else {
+ currentTab?.loadFromInitializer(homePageInitializer)
+ }
+ }
- logger.log(TAG, "New tab, show: $show")
+ /**
+ * Call when the user clicks on the tab menu located in the tab drawer.
+ */
+ fun onTabMenuClick() {
+ currentTab?.let {
+ view?.showCloseBrowserDialog(it.id)
+ }
+ }
- val startingTab = tabsModel.newTab(view as Activity, tabInitializer, isIncognito)
- if (tabsModel.size() == 1) {
- startingTab.resumeTimers()
+ /**
+ * Call when the user clicks on the bookmark menu (star or back arrow) located in the bookmark
+ * drawer.
+ */
+ fun onBookmarkMenuClick() {
+ if (currentFolder != Bookmark.Folder.Root) {
+ currentFolder = Bookmark.Folder.Root
+ compositeDisposable += bookmarkRepository
+ .bookmarksAndFolders(folder = Bookmark.Folder.Root)
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy { list ->
+ view?.updateState(viewState.copy(bookmarks = list, isRootFolder = true))
+ }
}
+ }
- view.notifyTabViewAdded()
- view.updateTabNumber(tabsModel.size())
+ /**
+ * Call when the user long presses anywhere on the web page with the provided tab [id].
+ */
+ fun onPageLongPress(id: Int, longPress: LongPress) {
+ val pageUrl = model.tabsList.find { it.id == id }?.url
+ if (pageUrl?.isSpecialUrl() == true) {
+ val url = longPress.targetUrl ?: return
+ if (pageUrl.isBookmarkUrl()) {
+ if (url.isBookmarkUrl()) {
+ val filename = requireNotNull(longPress.targetUrl.toUri().lastPathSegment) {
+ "Last segment should always exist for bookmark file"
+ }
+ val folderTitle = filename.substring(
+ 0,
+ filename.length - BookmarkPageFactory.FILENAME.length - 1
+ )
+ view?.showFolderOptionsDialog(folderTitle.asFolder())
+ } else {
+ compositeDisposable += bookmarkRepository.findBookmarkForUrl(url)
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy {
+ view?.showBookmarkOptionsDialog(it)
+ }
+ }
+ } else if (pageUrl.isDownloadsUrl()) {
+ compositeDisposable += downloadsRepository.findDownloadForUrl(url)
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy {
+ view?.showDownloadOptionsDialog(it)
+ }
+ } else if (pageUrl.isHistoryUrl()) {
+ compositeDisposable += historyRepository.findHistoryEntriesContaining(url)
+ .subscribeOn(databaseScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy { entries ->
+ entries.firstOrNull()?.let {
+ view?.showHistoryOptionsDialog(it)
+ } ?: view?.showHistoryOptionsDialog(HistoryEntry(url = url, title = ""))
+ }
+
+ }
+ } else {
+ when (longPress.hitCategory) {
+ LongPress.Category.IMAGE -> view?.showImageLongPressDialog(longPress)
+ LongPress.Category.LINK -> view?.showLinkLongPressDialog(longPress)
+ LongPress.Category.UNKNOWN -> Unit // Do nothing
+ }
+ }
+ }
- if (show) {
- onTabChanged(tabsModel.switchToTab(tabsModel.last()))
+ /**
+ * Call when the user selects an option from the close browser menu that can be invoked by long
+ * pressing on individual tabs.
+ */
+ fun onCloseBrowserEvent(id: Int, closeTabEvent: BrowserContract.CloseTabEvent) {
+ when (closeTabEvent) {
+ BrowserContract.CloseTabEvent.CLOSE_CURRENT ->
+ onTabClose(tabListState.tabIndexForId(id))
+ BrowserContract.CloseTabEvent.CLOSE_OTHERS -> model.tabsList
+ .filter { it.id != id }
+ .toObservable()
+ .flatMapCompletable { model.deleteTab(it.id) }
+ .subscribeOn(mainScheduler)
+ .subscribe()
+ BrowserContract.CloseTabEvent.CLOSE_ALL ->
+ compositeDisposable += model.deleteAllTabs().subscribeOn(mainScheduler)
+ .subscribeBy(onComplete = navigator::closeBrowser)
}
+ }
- return true
+ /**
+ * Call when the user long presses on a link within the web page and selects what they want to
+ * do with that link.
+ */
+ fun onLinkLongPressEvent(
+ longPress: LongPress,
+ linkLongPressEvent: BrowserContract.LinkLongPressEvent
+ ) {
+ when (linkLongPressEvent) {
+ BrowserContract.LinkLongPressEvent.NEW_TAB ->
+ longPress.targetUrl?.let {
+ createNewTabAndSelect(
+ UrlInitializer(it),
+ shouldSelect = true
+ )
+ }
+ BrowserContract.LinkLongPressEvent.BACKGROUND_TAB ->
+ longPress.targetUrl?.let {
+ createNewTabAndSelect(
+ UrlInitializer(it),
+ shouldSelect = false
+ )
+ }
+ BrowserContract.LinkLongPressEvent.INCOGNITO_TAB -> longPress.targetUrl?.let(navigator::launchIncognito)
+ BrowserContract.LinkLongPressEvent.SHARE ->
+ longPress.targetUrl?.let { navigator.sharePage(url = it, title = null) }
+ BrowserContract.LinkLongPressEvent.COPY_LINK ->
+ longPress.targetUrl?.let(navigator::copyPageLink)
+ }
}
- fun onAutoCompleteItemPressed() {
- tabsModel.currentTab?.requestFocus()
+ /**
+ * Call when the user long presses on an image within the web page and selects what they want to
+ * do with that image.
+ */
+ fun onImageLongPressEvent(
+ longPress: LongPress,
+ imageLongPressEvent: BrowserContract.ImageLongPressEvent
+ ) {
+ when (imageLongPressEvent) {
+ BrowserContract.ImageLongPressEvent.NEW_TAB ->
+ longPress.targetUrl?.let {
+ createNewTabAndSelect(
+ UrlInitializer(it),
+ shouldSelect = true
+ )
+ }
+ BrowserContract.ImageLongPressEvent.BACKGROUND_TAB ->
+ longPress.targetUrl?.let {
+ createNewTabAndSelect(
+ UrlInitializer(it),
+ shouldSelect = false
+ )
+ }
+ BrowserContract.ImageLongPressEvent.INCOGNITO_TAB -> longPress.targetUrl?.let(navigator::launchIncognito)
+ BrowserContract.ImageLongPressEvent.SHARE ->
+ longPress.targetUrl?.let { navigator.sharePage(url = it, title = null) }
+ BrowserContract.ImageLongPressEvent.COPY_LINK ->
+ longPress.targetUrl?.let(navigator::copyPageLink)
+ BrowserContract.ImageLongPressEvent.DOWNLOAD -> navigator.download(
+ PendingDownload(
+ url = longPress.targetUrl.orEmpty(),
+ userAgent = null,
+ contentDisposition = "attachment",
+ mimeType = null,
+ contentLength = 0
+ )
+ )
+ }
}
- fun findInPage(query: String): FindResults? {
- return tabsModel.currentTab?.find(query)
+ /**
+ * Call when the user has selected a file from the file chooser to upload.
+ */
+ fun onFileChooserResult(activityResult: ActivityResult) {
+ currentTab?.handleFileChooserResult(activityResult)
}
- companion object {
- private const val TAG = "BrowserPresenter"
+ private fun BrowserContract.View?.updateState(state: BrowserViewState) {
+ viewState = state
+ this?.renderState(viewState)
}
+ private fun BrowserContract.View?.updateTabs(tabs: List) {
+ tabListState = tabs
+ this?.renderTabs(tabListState)
+ }
}
diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserStateAdapter.kt b/app/src/main/java/acr/browser/lightning/browser/BrowserStateAdapter.kt
new file mode 100644
index 000000000..21398c87d
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/BrowserStateAdapter.kt
@@ -0,0 +1,160 @@
+package acr.browser.lightning.browser
+
+import acr.browser.lightning.browser.tab.TabViewState
+import acr.browser.lightning.database.Bookmark
+import acr.browser.lightning.database.HistoryEntry
+import acr.browser.lightning.database.downloads.DownloadEntry
+import acr.browser.lightning.ssl.SslCertificateInfo
+import acr.browser.lightning.ssl.showSslDialog
+import android.content.Intent
+import android.view.View
+import acr.browser.lightning.browser.view.targetUrl.LongPress
+
+/**
+ * An adapter between [BrowserContract.View] and the [BrowserActivity] that creates partial states
+ * to render in the activity.
+ */
+class BrowserStateAdapter(private val browserActivity: BrowserActivity) : BrowserContract.View {
+
+ private var currentState: BrowserViewState? = null
+ private var currentTabs: List? = null
+
+ override fun renderState(viewState: BrowserViewState) {
+ val (
+ displayUrl,
+ sslState,
+ isRefresh,
+ progress,
+ enableFullMenu,
+ themeColor,
+ isForwardEnabled,
+ isBackEnabled,
+ bookmarks,
+ isBookmarked,
+ isBookmarkEnabled,
+ isRootFolder,
+ findInPage
+ ) = viewState
+
+ browserActivity.renderState(
+ PartialBrowserViewState(
+ displayUrl = displayUrl.takeIf { it != currentState?.displayUrl },
+ sslState = sslState.takeIf { it != currentState?.sslState },
+ isRefresh = isRefresh.takeIf { it != currentState?.isRefresh },
+ progress = progress.takeIf { it != currentState?.progress },
+ enableFullMenu = enableFullMenu.takeIf { it != currentState?.enableFullMenu },
+ themeColor = themeColor.takeIf { it != currentState?.themeColor },
+ isForwardEnabled = isForwardEnabled.takeIf { it != currentState?.isForwardEnabled },
+ isBackEnabled = isBackEnabled.takeIf { it != currentState?.isBackEnabled },
+ bookmarks = bookmarks.takeIf { it != currentState?.bookmarks },
+ isBookmarked = isBookmarked.takeIf { it != currentState?.isBookmarked },
+ isBookmarkEnabled = isBookmarkEnabled.takeIf { it != currentState?.isBookmarkEnabled },
+ isRootFolder = isRootFolder.takeIf { it != currentState?.isRootFolder },
+ findInPage = findInPage.takeIf { it != currentState?.findInPage }
+ )
+ )
+
+ currentState = viewState
+ }
+
+ override fun renderTabs(tabs: List) {
+ tabs.takeIf { it != currentTabs }?.let(browserActivity::renderTabs)
+ }
+
+ override fun showAddBookmarkDialog(title: String, url: String, folders: List) {
+ browserActivity.showAddBookmarkDialog(title, url, folders)
+ }
+
+ override fun showBookmarkOptionsDialog(bookmark: Bookmark.Entry) {
+ browserActivity.showBookmarkOptionsDialog(bookmark)
+ }
+
+ override fun showEditBookmarkDialog(
+ title: String,
+ url: String,
+ folder: String,
+ folders: List
+ ) {
+ browserActivity.showEditBookmarkDialog(title, url, folder, folders)
+ }
+
+ override fun showFolderOptionsDialog(folder: Bookmark.Folder) {
+ browserActivity.showFolderOptionsDialog(folder)
+ }
+
+ override fun showEditFolderDialog(title: String) {
+ browserActivity.showEditFolderDialog(title)
+ }
+
+ override fun showDownloadOptionsDialog(download: DownloadEntry) {
+ browserActivity.showDownloadOptionsDialog(download)
+ }
+
+ override fun showHistoryOptionsDialog(historyEntry: HistoryEntry) {
+ browserActivity.showHistoryOptionsDialog(historyEntry)
+ }
+
+ override fun showFindInPageDialog() {
+ browserActivity.showFindInPageDialog()
+ }
+
+ override fun showLinkLongPressDialog(longPress: LongPress) {
+ browserActivity.showLinkLongPressDialog(longPress)
+ }
+
+ override fun showImageLongPressDialog(longPress: LongPress) {
+ browserActivity.showImageLongPressDialog(longPress)
+ }
+
+ override fun showSslDialog(sslCertificateInfo: SslCertificateInfo) {
+ browserActivity.showSslDialog(sslCertificateInfo)
+ }
+
+ override fun showCloseBrowserDialog(id: Int) {
+ browserActivity.showCloseBrowserDialog(id)
+ }
+
+ override fun openBookmarkDrawer() {
+ browserActivity.openBookmarkDrawer()
+ }
+
+ override fun closeBookmarkDrawer() {
+ browserActivity.closeBookmarkDrawer()
+ }
+
+ override fun openTabDrawer() {
+ browserActivity.openTabDrawer()
+ }
+
+ override fun closeTabDrawer() {
+ browserActivity.closeTabDrawer()
+ }
+
+ override fun showToolbar() {
+ browserActivity.showToolbar()
+ }
+
+ override fun showToolsDialog(areAdsAllowed: Boolean, shouldShowAdBlockOption: Boolean) {
+ browserActivity.showToolsDialog(areAdsAllowed, shouldShowAdBlockOption)
+ }
+
+ override fun showLocalFileBlockedDialog() {
+ browserActivity.showLocalFileBlockedDialog()
+ }
+
+ override fun showFileChooser(intent: Intent) {
+ browserActivity.showFileChooser(intent)
+ }
+
+ override fun showCustomView(view: View) {
+ browserActivity.showCustomView(view)
+ }
+
+ override fun hideCustomView() {
+ browserActivity.hideCustomView()
+ }
+
+ override fun clearSearchFocus() {
+ browserActivity.clearSearchFocus()
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserView.kt b/app/src/main/java/acr/browser/lightning/browser/BrowserView.kt
deleted file mode 100644
index e95891c86..000000000
--- a/app/src/main/java/acr/browser/lightning/browser/BrowserView.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package acr.browser.lightning.browser
-
-import acr.browser.lightning.ssl.SslState
-import android.view.View
-import androidx.annotation.StringRes
-
-interface BrowserView {
-
- fun setTabView(view: View)
-
- fun removeTabView()
-
- fun updateUrl(url: String?, isLoading: Boolean)
-
- fun updateProgress(progress: Int)
-
- fun updateTabNumber(number: Int)
-
- fun updateSslState(sslState: SslState)
-
- fun closeBrowser()
-
- fun closeActivity()
-
- fun showBlockedLocalFileDialog(onPositiveClick: () -> Unit)
-
- fun showSnackbar(@StringRes resource: Int)
-
- fun setForwardButtonEnabled(enabled: Boolean)
-
- fun setBackButtonEnabled(enabled: Boolean)
-
- fun notifyTabViewRemoved(position: Int)
-
- fun notifyTabViewAdded()
-
- fun notifyTabViewChanged(position: Int)
-
- fun notifyTabViewInitialized()
-
-}
diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserViewState.kt b/app/src/main/java/acr/browser/lightning/browser/BrowserViewState.kt
new file mode 100644
index 000000000..28d4810da
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/BrowserViewState.kt
@@ -0,0 +1,75 @@
+package acr.browser.lightning.browser
+
+import acr.browser.lightning.database.Bookmark
+import acr.browser.lightning.ssl.SslState
+import acr.browser.lightning.utils.Option
+
+/**
+ * The browser view state.
+ *
+ * @param displayUrl The current text shown in the search box.
+ * @param sslState The current SSL state shown in the search box.
+ * @param isRefresh True if the refresh button shows a refresh icon, false if it shows an X.
+ * @param progress The current page loading progress out of 100.
+ * @param enableFullMenu True if the full options menu should be shown, false if the limited one
+ * should be shown for a local web page.
+ * @param themeColor The UI theme as determined from the current web page.
+ * @param isForwardEnabled True if the go forward button should be enabled, false otherwise.
+ * @param isBackEnabled True if the go back button should be enabled, false otherwise.
+ * @param bookmarks The current list of bookmarks that is displayed.
+ * @param isBookmarked True if the current page is bookmarked, false otherwise.
+ * @param isBookmarkEnabled True if the user should be allowed to bookmark the current page, false
+ * otherwise.
+ * @param isRootFolder True if the current bookmark folder is the root folder, false if it is a
+ * child folder.
+ * @param findInPage The text that we are searching the page for.
+ */
+data class BrowserViewState(
+ // search bar
+ val displayUrl: String,
+ val sslState: SslState,
+ val isRefresh: Boolean,
+ val progress: Int,
+ val enableFullMenu: Boolean,
+ val themeColor: Option,
+
+ // Tabs
+ val isForwardEnabled: Boolean,
+ val isBackEnabled: Boolean,
+
+ // Bookmarks
+ val bookmarks: List,
+ val isBookmarked: Boolean,
+ val isBookmarkEnabled: Boolean,
+ val isRootFolder: Boolean,
+
+ // find
+ val findInPage: String
+
+)
+
+/**
+ * A partial copy of [BrowserViewState], where null indicates that the value is unchanged.
+ */
+data class PartialBrowserViewState(
+ // search bar
+ val displayUrl: String?,
+ val sslState: SslState?,
+ val isRefresh: Boolean?,
+ val progress: Int?,
+ val enableFullMenu: Boolean?,
+ val themeColor: Option?,
+
+ // Tabs
+ val isForwardEnabled: Boolean?,
+ val isBackEnabled: Boolean?,
+
+ // Bookmarks
+ val bookmarks: List?,
+ val isBookmarked: Boolean?,
+ val isBookmarkEnabled: Boolean?,
+ val isRootFolder: Boolean?,
+
+ // find
+ val findInPage: String?
+)
diff --git a/app/src/main/java/acr/browser/lightning/browser/TabsManager.kt b/app/src/main/java/acr/browser/lightning/browser/TabsManager.kt
deleted file mode 100644
index c9b24df4d..000000000
--- a/app/src/main/java/acr/browser/lightning/browser/TabsManager.kt
+++ /dev/null
@@ -1,433 +0,0 @@
-package acr.browser.lightning.browser
-
-import acr.browser.lightning.R
-import acr.browser.lightning.di.DatabaseScheduler
-import acr.browser.lightning.di.DiskScheduler
-import acr.browser.lightning.di.MainScheduler
-import acr.browser.lightning.log.Logger
-import acr.browser.lightning.search.SearchEngineProvider
-import acr.browser.lightning.utils.*
-import acr.browser.lightning.view.*
-import android.app.Activity
-import android.app.Application
-import android.app.SearchManager
-import android.content.Intent
-import android.os.Bundle
-import android.webkit.URLUtil
-import io.reactivex.Maybe
-import io.reactivex.Observable
-import io.reactivex.Scheduler
-import io.reactivex.Single
-import javax.inject.Inject
-
-/**
- * A manager singleton that holds all the [LightningView] and tracks the current tab. It handles
- * creation, deletion, restoration, state saving, and switching of tabs.
- */
-class TabsManager @Inject constructor(
- private val application: Application,
- private val searchEngineProvider: SearchEngineProvider,
- @DatabaseScheduler private val databaseScheduler: Scheduler,
- @DiskScheduler private val diskScheduler: Scheduler,
- @MainScheduler private val mainScheduler: Scheduler,
- private val homePageInitializer: HomePageInitializer,
- private val bookmarkPageInitializer: BookmarkPageInitializer,
- private val historyPageInitializer: HistoryPageInitializer,
- private val downloadPageInitializer: DownloadPageInitializer,
- private val logger: Logger
-) {
-
- private val tabList = arrayListOf()
-
- /**
- * Return the current [LightningView] or null if no current tab has been set.
- *
- * @return a [LightningView] or null if there is no current tab.
- */
- var currentTab: LightningView? = null
- private set
-
- private var tabNumberListeners = emptySet<(Int) -> Unit>()
-
- private var isInitialized = false
- private var postInitializationWorkList = emptyList<() -> Unit>()
-
- /**
- * Adds a listener to be notified when the number of tabs changes.
- */
- fun addTabNumberChangedListener(listener: ((Int) -> Unit)) {
- tabNumberListeners += listener
- }
-
- /**
- * Cancels any pending work that was scheduled to run after initialization.
- */
- fun cancelPendingWork() {
- postInitializationWorkList = emptyList()
- }
-
- /**
- * Executes the [runnable] after the manager has been initialized.
- */
- fun doAfterInitialization(runnable: () -> Unit) {
- if (isInitialized) {
- runnable()
- } else {
- postInitializationWorkList += runnable
- }
- }
-
- private fun finishInitialization() {
- isInitialized = true
- for (runnable in postInitializationWorkList) {
- runnable()
- }
- }
-
- /**
- * Initialize the state of the [TabsManager] based on previous state of the browser and with the
- * new provided [intent] and emit the last tab that should be displayed. By default operates on
- * a background scheduler and emits on the foreground scheduler.
- */
- fun initializeTabs(activity: Activity, intent: Intent?, incognito: Boolean): Single =
- Single
- .just(Option.fromNullable(
- if (intent?.action == Intent.ACTION_WEB_SEARCH) {
- extractSearchFromIntent(intent)
- } else {
- intent?.dataString
- }
- ))
- .doOnSuccess { shutdown() }
- .subscribeOn(mainScheduler)
- .observeOn(databaseScheduler)
- .flatMapObservable {
- if (incognito) {
- initializeIncognitoMode(it.value())
- } else {
- initializeRegularMode(it.value(), activity)
- }
- }
- .observeOn(mainScheduler)
- .map { newTab(activity, it, incognito) }
- .lastOrError()
- .doAfterSuccess { finishInitialization() }
-
- /**
- * Returns an [Observable] that emits the [TabInitializer] for incognito mode.
- */
- private fun initializeIncognitoMode(initialUrl: String?): Observable =
- Observable.fromCallable { initialUrl?.let(::UrlInitializer) ?: homePageInitializer }
-
- /**
- * Returns an [Observable] that emits the [TabInitializer] for normal operation mode.
- */
- private fun initializeRegularMode(initialUrl: String?, activity: Activity): Observable =
- restorePreviousTabs()
- .concatWith(Maybe.fromCallable {
- return@fromCallable initialUrl?.let {
- if (URLUtil.isFileUrl(it)) {
- PermissionInitializer(it, activity, homePageInitializer)
- } else {
- UrlInitializer(it)
- }
- }
- })
- .defaultIfEmpty(homePageInitializer)
-
- /**
- * Returns the URL for a search [Intent]. If the query is empty, then a null URL will be
- * returned.
- */
- fun extractSearchFromIntent(intent: Intent): String? {
- val query = intent.getStringExtra(SearchManager.QUERY)
- val searchUrl = "${searchEngineProvider.provideSearchEngine().queryUrl}$QUERY_PLACE_HOLDER"
-
- return if (query?.isNotBlank() == true) {
- smartUrlFilter(query, true, searchUrl)
- } else {
- null
- }
- }
-
- /**
- * Returns an observable that emits the [TabInitializer] for each previously opened tab as
- * saved on disk. Can potentially be empty.
- */
- private fun restorePreviousTabs(): Observable = readSavedStateFromDisk()
- .map { (bundle, title) ->
- return@map bundle.getString(URL_KEY)?.let { url ->
- when {
- url.isBookmarkUrl() -> bookmarkPageInitializer
- url.isDownloadsUrl() -> downloadPageInitializer
- url.isStartPageUrl() -> homePageInitializer
- url.isHistoryUrl() -> historyPageInitializer
- else -> homePageInitializer
- }
- } ?: FreezableBundleInitializer(bundle, title
- ?: application.getString(R.string.tab_frozen))
- }
-
-
- /**
- * Method used to resume all the tabs in the browser. This is necessary because we cannot pause
- * the WebView when the application is open currently due to a bug in the WebView, where calling
- * onResume doesn't consistently resume it.
- */
- fun resumeAll() {
- currentTab?.resumeTimers()
- for (tab in tabList) {
- tab.onResume()
- tab.initializePreferences()
- }
- }
-
- /**
- * Method used to pause all the tabs in the browser. This is necessary because we cannot pause
- * the WebView when the application is open currently due to a bug in the WebView, where calling
- * onResume doesn't consistently resume it.
- */
- fun pauseAll() {
- currentTab?.pauseTimers()
- tabList.forEach(LightningView::onPause)
- }
-
- /**
- * Return the tab at the given position in tabs list, or null if position is not in tabs list
- * range.
- *
- * @param position the index in tabs list
- * @return the corespondent [LightningView], or null if the index is invalid
- */
- fun getTabAtPosition(position: Int): LightningView? =
- if (position < 0 || position >= tabList.size) {
- null
- } else {
- tabList[position]
- }
-
- val allTabs: List
- get() = tabList
-
- /**
- * Shutdown the manager. This destroys all tabs and clears the references to those tabs. Current
- * tab is also released for garbage collection.
- */
- fun shutdown() {
- repeat(tabList.size) { deleteTab(0) }
- isInitialized = false
- currentTab = null
- }
-
- /**
- * The current number of tabs in the manager.
- *
- * @return the number of tabs in the list.
- */
- fun size(): Int = tabList.size
-
- /**
- * The index of the last tab in the manager.
- *
- * @return the last tab in the list or -1 if there are no tabs.
- */
- fun last(): Int = tabList.size - 1
-
-
- /**
- * The last tab in the tab manager.
- *
- * @return the last tab, or null if there are no tabs.
- */
- fun lastTab(): LightningView? = tabList.lastOrNull()
-
- /**
- * Create and return a new tab. The tab is automatically added to the tabs list.
- *
- * @param activity the activity needed to create the tab.
- * @param tabInitializer the initializer to run on the tab after it's been created.
- * @param isIncognito whether the tab is an incognito tab or not.
- * @return a valid initialized tab.
- */
- fun newTab(
- activity: Activity,
- tabInitializer: TabInitializer,
- isIncognito: Boolean
- ): LightningView {
- logger.log(TAG, "New tab")
- val tab = LightningView(
- activity,
- tabInitializer,
- isIncognito,
- homePageInitializer,
- bookmarkPageInitializer,
- downloadPageInitializer,
- logger
- )
- tabList.add(tab)
- tabNumberListeners.forEach { it(size()) }
- return tab
- }
-
- /**
- * Removes a tab from the list and destroys the tab. If the tab removed is the current tab, the
- * reference to the current tab will be nullified.
- *
- * @param position The position of the tab to remove.
- */
- private fun removeTab(position: Int) {
- if (position >= tabList.size) {
- return
- }
- val tab = tabList.removeAt(position)
- if (currentTab == tab) {
- currentTab = null
- }
- tab.onDestroy()
- }
-
- /**
- * Deletes a tab from the manager. If the tab being deleted is the current tab, this method will
- * switch the current tab to a new valid tab.
- *
- * @param position the position of the tab to delete.
- * @return returns true if the current tab was deleted, false otherwise.
- */
- fun deleteTab(position: Int): Boolean {
- logger.log(TAG, "Delete tab: $position")
- val currentTab = currentTab
- val current = positionOf(currentTab)
-
- if (current == position) {
- when {
- size() == 1 -> this.currentTab = null
- current < size() - 1 -> switchToTab(current + 1)
- else -> switchToTab(current - 1)
- }
- }
-
- removeTab(position)
- tabNumberListeners.forEach { it(size()) }
- return current == position
- }
-
- /**
- * Return the position of the given tab.
- *
- * @param tab the tab to look for.
- * @return the position of the tab or -1 if the tab is not in the list.
- */
- fun positionOf(tab: LightningView?): Int = tabList.indexOf(tab)
-
- /**
- * Saves the state of the current WebViews, to a bundle which is then stored in persistent
- * storage and can be unparceled.
- */
- fun saveState() {
- val outState = Bundle(ClassLoader.getSystemClassLoader())
- logger.log(TAG, "Saving tab state")
- tabList
- .withIndex()
- .forEach { (index, tab) ->
- if (!tab.url.isSpecialUrl()) {
- outState.putBundle(BUNDLE_KEY + index, tab.saveState())
- outState.putString(TAB_TITLE_KEY + index, tab.title)
- } else {
- outState.putBundle(BUNDLE_KEY + index, Bundle().apply {
- putString(URL_KEY, tab.url)
- })
- }
- }
- FileUtils.writeBundleToStorage(application, outState, BUNDLE_STORAGE)
- .subscribeOn(diskScheduler)
- .subscribe()
- }
-
- /**
- * Use this method to clear the saved state if you do not wish it to be restored when the
- * browser next starts.
- */
- fun clearSavedState() = FileUtils.deleteBundleInStorage(application, BUNDLE_STORAGE)
-
- /**
- * Creates an [Observable] that emits the [Bundle] state stored for each previously opened tab
- * on disk. After the list of bundle [Bundle] is read off disk, the old state will be deleted.
- * Can potentially be empty.
- */
- private fun readSavedStateFromDisk(): Observable> = Maybe
- .fromCallable { FileUtils.readBundleFromStorage(application, BUNDLE_STORAGE) }
- .flattenAsObservable { bundle ->
- bundle.keySet()
- .filter { it.startsWith(BUNDLE_KEY) }
- .mapNotNull { bundleKey ->
- bundle.getBundle(bundleKey)?.let {
- Pair(
- it,
- bundle.getString(TAB_TITLE_KEY + bundleKey.extractNumberFromEnd())
- )
- }
- }
- }
- .doOnNext { logger.log(TAG, "Restoring previous WebView state now") }
-
- private fun String.extractNumberFromEnd(): String {
- val underScore = lastIndexOf('_')
- return if (underScore in 0 until length) {
- substring(underScore + 1)
- } else {
- ""
- }
- }
-
- /**
- * Returns the index of the current tab.
- *
- * @return Return the index of the current tab, or -1 if the current tab is null.
- */
- fun indexOfCurrentTab(): Int = tabList.indexOf(currentTab)
-
- /**
- * Returns the index of the tab.
- *
- * @return Return the index of the tab, or -1 if the tab isn't in the list.
- */
- fun indexOfTab(tab: LightningView): Int = tabList.indexOf(tab)
-
- /**
- * Returns the [LightningView] with the provided hash, or null if there is no tab with the hash.
- *
- * @param hashCode the hashcode.
- * @return the tab with an identical hash, or null.
- */
- fun getTabForHashCode(hashCode: Int): LightningView? =
- tabList.firstOrNull { lightningView -> lightningView.webView?.let { it.hashCode() == hashCode } == true }
-
- /**
- * Switch the current tab to the one at the given position. It returns the selected tab that has
- * been switched to.
- *
- * @return the selected tab or null if position is out of tabs range.
- */
- fun switchToTab(position: Int): LightningView? {
- logger.log(TAG, "switch to tab: $position")
- return if (position < 0 || position >= tabList.size) {
- logger.log(TAG, "Returning a null LightningView requested for position: $position")
- null
- } else {
- tabList[position].also {
- currentTab = it
- }
- }
- }
-
- companion object {
-
- private const val TAG = "TabsManager"
-
- private const val BUNDLE_KEY = "WEBVIEW_"
- private const val TAB_TITLE_KEY = "TITLE_"
- private const val URL_KEY = "URL_KEY"
- private const val BUNDLE_STORAGE = "SAVED_TABS.parcel"
- }
-
-}
diff --git a/app/src/main/java/acr/browser/lightning/browser/TabsView.kt b/app/src/main/java/acr/browser/lightning/browser/TabsView.kt
deleted file mode 100644
index 18ea12d3b..000000000
--- a/app/src/main/java/acr/browser/lightning/browser/TabsView.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package acr.browser.lightning.browser
-
-/**
- * The interface for communicating to the tab list view.
- */
-interface TabsView {
-
- /**
- * Called when a tab has been added.
- */
- fun tabAdded()
-
- /**
- * Called when a tab has been removed.
- *
- * @param position the position of the tab that has been removed.
- */
- fun tabRemoved(position: Int)
-
- /**
- * Called when a tab's metadata has been changed.
- *
- * @param position the position of the tab that has been changed.
- */
- fun tabChanged(position: Int)
-
- /**
- * Called when the tabs are completely initialized for the first time.
- */
- fun tabsInitialized()
-
- /**
- * Enables and disables the go back button.
- */
- fun setGoBackEnabled(isEnabled: Boolean)
-
- /**
- * Enables and disables the go forward button.
- */
- fun setGoForwardEnabled(isEnabled: Boolean)
-}
diff --git a/app/src/main/java/acr/browser/lightning/browser/activity/BrowserActivity.kt b/app/src/main/java/acr/browser/lightning/browser/activity/BrowserActivity.kt
deleted file mode 100644
index 8804a33e9..000000000
--- a/app/src/main/java/acr/browser/lightning/browser/activity/BrowserActivity.kt
+++ /dev/null
@@ -1,1809 +0,0 @@
-/*
- * Copyright 2015 Anthony Restaino
- */
-
-package acr.browser.lightning.browser.activity
-
-import acr.browser.lightning.AppTheme
-import acr.browser.lightning.IncognitoActivity
-import acr.browser.lightning.R
-import acr.browser.lightning.browser.*
-import acr.browser.lightning.browser.bookmarks.BookmarksDrawerView
-import acr.browser.lightning.browser.cleanup.ExitCleanup
-import acr.browser.lightning.browser.tabs.TabsDesktopView
-import acr.browser.lightning.browser.tabs.TabsDrawerView
-import acr.browser.lightning.controller.UIController
-import acr.browser.lightning.database.Bookmark
-import acr.browser.lightning.database.HistoryEntry
-import acr.browser.lightning.database.SearchSuggestion
-import acr.browser.lightning.database.WebPage
-import acr.browser.lightning.database.bookmark.BookmarkRepository
-import acr.browser.lightning.database.history.HistoryRepository
-import acr.browser.lightning.di.*
-import acr.browser.lightning.dialog.BrowserDialog
-import acr.browser.lightning.dialog.DialogItem
-import acr.browser.lightning.dialog.LightningDialogBuilder
-import acr.browser.lightning.extensions.*
-import acr.browser.lightning.html.bookmark.BookmarkPageFactory
-import acr.browser.lightning.html.history.HistoryPageFactory
-import acr.browser.lightning.html.homepage.HomePageFactory
-import acr.browser.lightning.icon.TabCountView
-import acr.browser.lightning.interpolator.BezierDecelerateInterpolator
-import acr.browser.lightning.log.Logger
-import acr.browser.lightning.notifications.IncognitoNotification
-import acr.browser.lightning.reading.activity.ReadingActivity
-import acr.browser.lightning.search.SearchEngineProvider
-import acr.browser.lightning.search.SuggestionsAdapter
-import acr.browser.lightning.settings.activity.SettingsActivity
-import acr.browser.lightning.ssl.SslState
-import acr.browser.lightning.ssl.createSslDrawableForState
-import acr.browser.lightning.ssl.showSslDialog
-import acr.browser.lightning.utils.*
-import acr.browser.lightning.view.*
-import acr.browser.lightning.view.SearchView
-import acr.browser.lightning.view.find.FindResults
-import android.app.Activity
-import android.app.NotificationManager
-import android.content.ClipboardManager
-import android.content.Intent
-import android.content.res.Configuration
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.graphics.drawable.Drawable
-import android.media.MediaPlayer
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.os.Handler
-import android.os.Message
-import android.provider.MediaStore
-import android.view.*
-import android.view.View.*
-import android.view.ViewGroup.LayoutParams
-import android.view.animation.Animation
-import android.view.animation.Transformation
-import android.view.inputmethod.EditorInfo
-import android.view.inputmethod.InputMethodManager
-import android.webkit.ValueCallback
-import android.webkit.WebChromeClient.CustomViewCallback
-import android.widget.*
-import android.widget.AdapterView.OnItemClickListener
-import android.widget.TextView.OnEditorActionListener
-import androidx.annotation.ColorInt
-import androidx.annotation.StringRes
-import androidx.appcompat.app.AlertDialog
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.content.ContextCompat
-import androidx.core.graphics.drawable.toBitmap
-import androidx.core.net.toUri
-import androidx.core.view.GravityCompat
-import androidx.drawerlayout.widget.DrawerLayout
-import androidx.palette.graphics.Palette
-import butterknife.ButterKnife
-import com.anthonycr.grant.PermissionsManager
-import io.reactivex.Completable
-import io.reactivex.Scheduler
-import io.reactivex.rxkotlin.subscribeBy
-import kotlinx.android.synthetic.main.activity_main.*
-import kotlinx.android.synthetic.main.browser_content.*
-import kotlinx.android.synthetic.main.search.*
-import kotlinx.android.synthetic.main.search_interface.*
-import kotlinx.android.synthetic.main.toolbar.*
-import java.io.IOException
-import javax.inject.Inject
-import kotlin.system.exitProcess
-
-abstract class BrowserActivity : ThemableBrowserActivity(), BrowserView, UIController, OnClickListener {
-
- // Toolbar Views
- private var searchBackground: View? = null
- private var searchView: SearchView? = null
- private var homeImageView: ImageView? = null
- private var tabCountView: TabCountView? = null
-
- // Current tab view being displayed
- private var currentTabView: View? = null
-
- // Full Screen Video Views
- private var fullscreenContainerView: FrameLayout? = null
- private var videoView: VideoView? = null
- private var customView: View? = null
-
- // Adapter
- private var suggestionsAdapter: SuggestionsAdapter? = null
-
- // Callback
- private var customViewCallback: CustomViewCallback? = null
- private var uploadMessageCallback: ValueCallback? = null
- private var filePathCallback: ValueCallback>? = null
-
- // Primitives
- private var isFullScreen: Boolean = false
- private var hideStatusBar: Boolean = false
- private var isDarkTheme: Boolean = false
- private var isImmersiveMode = false
- private var shouldShowTabsInDrawer: Boolean = false
- private var swapBookmarksAndTabs: Boolean = false
-
- private var originalOrientation: Int = 0
- private var currentUiColor = Color.BLACK
- private var keyDownStartTime: Long = 0
- private var searchText: String? = null
- private var cameraPhotoPath: String? = null
-
- private var findResult: FindResults? = null
-
- // The singleton BookmarkManager
- @Inject lateinit var bookmarkManager: BookmarkRepository
- @Inject lateinit var historyModel: HistoryRepository
- @Inject lateinit var searchBoxModel: SearchBoxModel
- @Inject lateinit var searchEngineProvider: SearchEngineProvider
- @Inject lateinit var inputMethodManager: InputMethodManager
- @Inject lateinit var clipboardManager: ClipboardManager
- @Inject lateinit var notificationManager: NotificationManager
- @Inject @field:DiskScheduler lateinit var diskScheduler: Scheduler
- @Inject @field:DatabaseScheduler lateinit var databaseScheduler: Scheduler
- @Inject @field:MainScheduler lateinit var mainScheduler: Scheduler
- @Inject lateinit var tabsManager: TabsManager
- @Inject lateinit var homePageFactory: HomePageFactory
- @Inject lateinit var bookmarkPageFactory: BookmarkPageFactory
- @Inject lateinit var historyPageFactory: HistoryPageFactory
- @Inject lateinit var historyPageInitializer: HistoryPageInitializer
- @Inject lateinit var downloadPageInitializer: DownloadPageInitializer
- @Inject lateinit var homePageInitializer: HomePageInitializer
- @Inject @field:MainHandler lateinit var mainHandler: Handler
- @Inject lateinit var proxyUtils: ProxyUtils
- @Inject lateinit var logger: Logger
- @Inject lateinit var bookmarksDialogBuilder: LightningDialogBuilder
- @Inject lateinit var exitCleanup: ExitCleanup
-
- // Image
- private var webPageBitmap: Bitmap? = null
- private val backgroundDrawable = ColorDrawable()
- private var incognitoNotification: IncognitoNotification? = null
-
- private var presenter: BrowserPresenter? = null
- private var tabsView: TabsView? = null
- private var bookmarksView: BookmarksView? = null
-
- // Menu
- private var backMenuItem: MenuItem? = null
- private var forwardMenuItem: MenuItem? = null
-
- private val longPressBackRunnable = Runnable {
- showCloseDialog(tabsManager.positionOf(tabsManager.currentTab))
- }
-
- /**
- * Determines if the current browser instance is in incognito mode or not.
- */
- protected abstract fun isIncognito(): Boolean
-
- /**
- * Choose the behavior when the controller closes the view.
- */
- abstract override fun closeActivity()
-
- /**
- * Choose what to do when the browser visits a website.
- *
- * @param title the title of the site visited.
- * @param url the url of the site visited.
- */
- abstract override fun updateHistory(title: String?, url: String)
-
- /**
- * An observable which asynchronously updates the user's cookie preferences.
- */
- protected abstract fun updateCookiePreference(): Completable
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- injector.inject(this)
- setContentView(R.layout.activity_main)
- ButterKnife.bind(this)
-
- if (isIncognito()) {
- incognitoNotification = IncognitoNotification(this, notificationManager)
- }
- tabsManager.addTabNumberChangedListener {
- if (isIncognito()) {
- if (it == 0) {
- incognitoNotification?.hide()
- } else {
- incognitoNotification?.show(it)
- }
- }
- }
-
- presenter = BrowserPresenter(
- this,
- isIncognito(),
- userPreferences,
- tabsManager,
- mainScheduler,
- homePageFactory,
- bookmarkPageFactory,
- RecentTabModel(),
- logger
- )
-
- initialize(savedInstanceState)
- }
-
- private fun initialize(savedInstanceState: Bundle?) {
- initializeToolbarHeight(resources.configuration)
- setSupportActionBar(toolbar)
- val actionBar = requireNotNull(supportActionBar)
-
- //TODO make sure dark theme flag gets set correctly
- isDarkTheme = userPreferences.useTheme != AppTheme.LIGHT || isIncognito()
- shouldShowTabsInDrawer = userPreferences.showTabsInDrawer
- swapBookmarksAndTabs = userPreferences.bookmarksAndTabsSwapped
-
- // initialize background ColorDrawable
- val primaryColor = ThemeUtils.getPrimaryColor(this)
- backgroundDrawable.color = primaryColor
-
- // Drawer stutters otherwise
- left_drawer.setLayerType(LAYER_TYPE_NONE, null)
- right_drawer.setLayerType(LAYER_TYPE_NONE, null)
-
- setNavigationDrawerWidth()
- drawer_layout.addDrawerListener(DrawerLocker())
-
- webPageBitmap = drawable(R.drawable.ic_webpage).toBitmap()
-
- tabsView = if (shouldShowTabsInDrawer) {
- TabsDrawerView(this).also(findViewById(getTabsContainerId())::addView)
- } else {
- TabsDesktopView(this).also(findViewById(getTabsContainerId())::addView)
- }
-
- bookmarksView = BookmarksDrawerView(this).also(findViewById(getBookmarksContainerId())::addView)
-
- if (shouldShowTabsInDrawer) {
- tabs_toolbar_container.visibility = GONE
- }
-
- // set display options of the ActionBar
- actionBar.setDisplayShowTitleEnabled(false)
- actionBar.setDisplayShowHomeEnabled(false)
- actionBar.setDisplayShowCustomEnabled(true)
- actionBar.setCustomView(R.layout.toolbar_content)
-
- val customView = actionBar.customView
- customView.layoutParams = customView.layoutParams.apply {
- width = LayoutParams.MATCH_PARENT
- height = LayoutParams.MATCH_PARENT
- }
-
- tabCountView = customView.findViewById(R.id.tab_count_view)
- homeImageView = customView.findViewById(R.id.home_image_view)
- if (shouldShowTabsInDrawer && !isIncognito()) {
- tabCountView?.visibility = VISIBLE
- homeImageView?.visibility = GONE
- } else if (shouldShowTabsInDrawer) {
- tabCountView?.visibility = GONE
- homeImageView?.visibility = VISIBLE
- homeImageView?.setImageResource(R.drawable.incognito_mode)
- // Post drawer locking in case the activity is being recreated
- mainHandler.post { drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, getTabDrawer()) }
- } else {
- tabCountView?.visibility = GONE
- homeImageView?.visibility = VISIBLE
- homeImageView?.setImageResource(R.drawable.ic_action_home)
- // Post drawer locking in case the activity is being recreated
- mainHandler.post { drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, getTabDrawer()) }
- }
-
- // Post drawer locking in case the activity is being recreated
- mainHandler.post { drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, getBookmarkDrawer()) }
-
- customView.findViewById(R.id.home_button).setOnClickListener(this)
-
- // create the search EditText in the ToolBar
- searchView = customView.findViewById(R.id.search).apply {
- search_ssl_status.setOnClickListener {
- tabsManager.currentTab?.let { tab ->
- tab.sslCertificate?.let { showSslDialog(it, tab.currentSslState()) }
- }
- }
- search_ssl_status.updateVisibilityForContent()
- search_refresh.setImageResource(R.drawable.ic_action_refresh)
-
- val searchListener = SearchListenerClass()
- setOnKeyListener(searchListener)
- onFocusChangeListener = searchListener
- setOnEditorActionListener(searchListener)
- onPreFocusListener = searchListener
- addTextChangedListener(StyleRemovingTextWatcher())
-
- initializeSearchSuggestions(this)
- }
-
- search_refresh.setOnClickListener {
- if (searchView?.hasFocus() == true) {
- searchView?.setText("")
- } else {
- refreshOrStop()
- }
- }
-
- searchBackground = customView.findViewById(R.id.search_container).apply {
- // initialize search background color
- background.tint(getSearchBarColor(primaryColor, primaryColor))
- }
-
- drawer_layout.setDrawerShadow(R.drawable.drawer_right_shadow, GravityCompat.END)
- drawer_layout.setDrawerShadow(R.drawable.drawer_left_shadow, GravityCompat.START)
-
- var intent: Intent? = if (savedInstanceState == null) {
- intent
- } else {
- null
- }
-
- val launchedFromHistory = intent != null && intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
-
- if (intent?.action == INTENT_PANIC_TRIGGER) {
- setIntent(null)
- panicClean()
- } else {
- if (launchedFromHistory) {
- intent = null
- }
- presenter?.setupTabs(intent)
- setIntent(null)
- proxyUtils.checkForProxy(this)
- }
- }
-
- private fun getBookmarksContainerId(): Int = if (swapBookmarksAndTabs) {
- R.id.left_drawer
- } else {
- R.id.right_drawer
- }
-
- private fun getTabsContainerId(): Int = if (shouldShowTabsInDrawer) {
- if (swapBookmarksAndTabs) {
- R.id.right_drawer
- } else {
- R.id.left_drawer
- }
- } else {
- R.id.tabs_toolbar_container
- }
-
- private fun getBookmarkDrawer(): View = if (swapBookmarksAndTabs) {
- left_drawer
- } else {
- right_drawer
- }
-
- private fun getTabDrawer(): View = if (swapBookmarksAndTabs) {
- right_drawer
- } else {
- left_drawer
- }
-
- protected fun panicClean() {
- logger.log(TAG, "Closing browser")
- tabsManager.newTab(this, NoOpInitializer(), false)
- tabsManager.switchToTab(0)
- tabsManager.clearSavedState()
-
- historyPageFactory.deleteHistoryPage().subscribe()
- closeBrowser()
- // System exit needed in the case of receiving
- // the panic intent since finish() isn't completely
- // closing the browser
- exitProcess(1)
- }
-
- private inner class SearchListenerClass : OnKeyListener,
- OnEditorActionListener,
- OnFocusChangeListener,
- SearchView.PreFocusListener {
-
- override fun onKey(view: View, keyCode: Int, keyEvent: KeyEvent): Boolean {
- when (keyCode) {
- KeyEvent.KEYCODE_ENTER -> {
- searchView?.let {
- inputMethodManager.hideSoftInputFromWindow(it.windowToken, 0)
- searchTheWeb(it.text.toString())
- }
-
- tabsManager.currentTab?.requestFocus()
- return true
- }
- else -> {
- }
- }
- return false
- }
-
- override fun onEditorAction(arg0: TextView, actionId: Int, arg2: KeyEvent?): Boolean {
- // hide the keyboard and search the web when the enter key
- // button is pressed
- if (actionId == EditorInfo.IME_ACTION_GO
- || actionId == EditorInfo.IME_ACTION_DONE
- || actionId == EditorInfo.IME_ACTION_NEXT
- || actionId == EditorInfo.IME_ACTION_SEND
- || actionId == EditorInfo.IME_ACTION_SEARCH
- || arg2?.action == KeyEvent.KEYCODE_ENTER) {
- searchView?.let {
- inputMethodManager.hideSoftInputFromWindow(it.windowToken, 0)
- searchTheWeb(it.text.toString())
- }
-
- tabsManager.currentTab?.requestFocus()
- return true
- }
- return false
- }
-
- override fun onFocusChange(v: View, hasFocus: Boolean) {
- val currentView = tabsManager.currentTab
- if (!hasFocus && currentView != null) {
- setIsLoading(currentView.progress < 100)
- updateUrl(currentView.url, false)
- } else if (hasFocus && currentView != null) {
-
- // Hack to make sure the text gets selected
- (v as SearchView).selectAll()
- search_ssl_status.visibility = GONE
- search_refresh.setImageResource(R.drawable.ic_action_delete)
- }
-
- if (!hasFocus) {
- search_ssl_status.updateVisibilityForContent()
- searchView?.let {
- inputMethodManager.hideSoftInputFromWindow(it.windowToken, 0)
- }
- }
- }
-
- override fun onPreFocus() {
- val currentView = tabsManager.currentTab ?: return
- val url = currentView.url
- if (!url.isSpecialUrl()) {
- if (searchView?.hasFocus() == false) {
- searchView?.setText(url)
- }
- }
- }
- }
-
- private inner class DrawerLocker : DrawerLayout.DrawerListener {
-
- override fun onDrawerClosed(v: View) {
- val tabsDrawer = getTabDrawer()
- val bookmarksDrawer = getBookmarkDrawer()
-
- if (v === tabsDrawer) {
- drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, bookmarksDrawer)
- } else if (shouldShowTabsInDrawer) {
- drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, tabsDrawer)
- }
- }
-
- override fun onDrawerOpened(v: View) {
- val tabsDrawer = getTabDrawer()
- val bookmarksDrawer = getBookmarkDrawer()
-
- if (v === tabsDrawer) {
- drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, bookmarksDrawer)
- } else {
- drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, tabsDrawer)
- }
- }
-
- override fun onDrawerSlide(v: View, arg: Float) = Unit
-
- override fun onDrawerStateChanged(arg: Int) = Unit
-
- }
-
- private fun setNavigationDrawerWidth() {
- val width = resources.displayMetrics.widthPixels - dimen(R.dimen.navigation_drawer_minimum_space)
- val maxWidth = resources.getDimensionPixelSize(R.dimen.navigation_drawer_max_width)
- if (width < maxWidth) {
- val params = left_drawer.layoutParams as DrawerLayout.LayoutParams
- params.width = width
- left_drawer.layoutParams = params
- left_drawer.requestLayout()
- val paramsRight = right_drawer.layoutParams as DrawerLayout.LayoutParams
- paramsRight.width = width
- right_drawer.layoutParams = paramsRight
- right_drawer.requestLayout()
- }
- }
-
- private fun initializePreferences() {
- val currentView = tabsManager.currentTab
- isFullScreen = userPreferences.fullScreenEnabled
-
- webPageBitmap?.let { webBitmap ->
- if (!isIncognito() && !isColorMode() && !isDarkTheme) {
- changeToolbarBackground(webBitmap, null)
- } else if (!isIncognito() && currentView != null && !isDarkTheme) {
- changeToolbarBackground(currentView.favicon ?: webBitmap, null)
- } else if (!isIncognito() && !isDarkTheme) {
- changeToolbarBackground(webBitmap, null)
- }
- }
-
- // TODO layout transition causing memory leak
- // content_frame.setLayoutTransition(new LayoutTransition());
-
- setFullscreen(userPreferences.hideStatusBarEnabled, false)
-
- val currentSearchEngine = searchEngineProvider.provideSearchEngine()
- searchText = currentSearchEngine.queryUrl
-
- updateCookiePreference().subscribeOn(diskScheduler).subscribe()
- proxyUtils.updateProxySettings(this)
- }
-
- public override fun onWindowVisibleToUserAfterResume() {
- super.onWindowVisibleToUserAfterResume()
- toolbar_layout.translationY = 0f
- setWebViewTranslation(toolbar_layout.height.toFloat())
- }
-
- override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
- if (keyCode == KeyEvent.KEYCODE_ENTER) {
- if (searchView?.hasFocus() == true) {
- searchView?.let { searchTheWeb(it.text.toString()) }
- }
- } else if (keyCode == KeyEvent.KEYCODE_BACK) {
- keyDownStartTime = System.currentTimeMillis()
- mainHandler.postDelayed(longPressBackRunnable, ViewConfiguration.getLongPressTimeout().toLong())
- }
- return super.onKeyDown(keyCode, event)
- }
-
- override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- mainHandler.removeCallbacks(longPressBackRunnable)
- if (System.currentTimeMillis() - keyDownStartTime > ViewConfiguration.getLongPressTimeout()) {
- return true
- }
- }
- return super.onKeyUp(keyCode, event)
- }
-
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- // Keyboard shortcuts
- if (event.action == KeyEvent.ACTION_DOWN) {
- when {
- event.isCtrlPressed -> when (event.keyCode) {
- KeyEvent.KEYCODE_F -> {
- // Search in page
- findInPage()
- return true
- }
- KeyEvent.KEYCODE_T -> {
- // Open new tab
- presenter?.newTab(
- homePageInitializer,
- true
- )
- return true
- }
- KeyEvent.KEYCODE_W -> {
- // Close current tab
- tabsManager.let { presenter?.deleteTab(it.indexOfCurrentTab()) }
- return true
- }
- KeyEvent.KEYCODE_Q -> {
- // Close browser
- closeBrowser()
- return true
- }
- KeyEvent.KEYCODE_R -> {
- // Refresh current tab
- tabsManager.currentTab?.reload()
- return true
- }
- KeyEvent.KEYCODE_TAB -> {
- tabsManager.let {
- val nextIndex = if (event.isShiftPressed) {
- // Go back one tab
- if (it.indexOfCurrentTab() > 0) {
- it.indexOfCurrentTab() - 1
- } else {
- it.last()
- }
- } else {
- // Go forward one tab
- if (it.indexOfCurrentTab() < it.last()) {
- it.indexOfCurrentTab() + 1
- } else {
- 0
- }
- }
-
- presenter?.tabChanged(nextIndex)
- }
-
- return true
- }
- }
- event.keyCode == KeyEvent.KEYCODE_SEARCH -> {
- // Highlight search field
- searchView?.requestFocus()
- searchView?.selectAll()
- return true
- }
- event.isAltPressed -> // Alt + tab number
- tabsManager.let {
- if (KeyEvent.KEYCODE_0 <= event.keyCode && event.keyCode <= KeyEvent.KEYCODE_9) {
- val nextIndex = if (event.keyCode > it.last() + KeyEvent.KEYCODE_1 || event.keyCode == KeyEvent.KEYCODE_0) {
- it.last()
- } else {
- event.keyCode - KeyEvent.KEYCODE_1
- }
- presenter?.tabChanged(nextIndex)
- return true
- }
- }
- }
- }
- return super.dispatchKeyEvent(event)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- val currentView = tabsManager.currentTab
- val currentUrl = currentView?.url
- // Handle action buttons
- when (item.itemId) {
- android.R.id.home -> {
- if (drawer_layout.isDrawerOpen(getBookmarkDrawer())) {
- drawer_layout.closeDrawer(getBookmarkDrawer())
- }
- return true
- }
- R.id.action_back -> {
- if (currentView?.canGoBack() == true) {
- currentView.goBack()
- }
- return true
- }
- R.id.action_forward -> {
- if (currentView?.canGoForward() == true) {
- currentView.goForward()
- }
- return true
- }
- R.id.action_add_to_homescreen -> {
- if (currentView != null
- && currentView.url.isNotBlank()
- && !currentView.url.isSpecialUrl()) {
- HistoryEntry(currentView.url, currentView.title).also {
- Utils.createShortcut(this, it, currentView.favicon ?: webPageBitmap!!)
- logger.log(TAG, "Creating shortcut: ${it.title} ${it.url}")
- }
- }
- return true
- }
- R.id.action_new_tab -> {
- presenter?.newTab(homePageInitializer, true)
- return true
- }
- R.id.action_incognito -> {
- startActivity(IncognitoActivity.createIntent(this))
- overridePendingTransition(R.anim.slide_up_in, R.anim.fade_out_scale)
- return true
- }
- R.id.action_share -> {
- IntentUtils(this).shareUrl(currentUrl, currentView?.title)
- return true
- }
- R.id.action_bookmarks -> {
- openBookmarks()
- return true
- }
- R.id.action_copy -> {
- if (currentUrl != null && !currentUrl.isSpecialUrl()) {
- clipboardManager.copyToClipboard(currentUrl)
- snackbar(R.string.message_link_copied)
- }
- return true
- }
- R.id.action_settings -> {
- startActivity(Intent(this, SettingsActivity::class.java))
- return true
- }
- R.id.action_history -> {
- openHistory()
- return true
- }
- R.id.action_downloads -> {
- openDownloads()
- return true
- }
- R.id.action_add_bookmark -> {
- if (currentUrl != null && !currentUrl.isSpecialUrl()) {
- addBookmark(currentView.title, currentUrl)
- }
- return true
- }
- R.id.action_find -> {
- findInPage()
- return true
- }
- R.id.action_reading_mode -> {
- if (currentUrl != null) {
- ReadingActivity.launch(this, currentUrl)
- }
- return true
- }
- else -> return super.onOptionsItemSelected(item)
- }
- }
-
- // By using a manager, adds a bookmark and notifies third parties about that
- private fun addBookmark(title: String, url: String) {
- val bookmark = Bookmark.Entry(url, title, 0, Bookmark.Folder.Root)
- bookmarksDialogBuilder.showAddBookmarkDialog(this, this, bookmark)
- }
-
- private fun deleteBookmark(title: String, url: String) {
- bookmarkManager.deleteBookmark(Bookmark.Entry(url, title, 0, Bookmark.Folder.Root))
- .subscribeOn(databaseScheduler)
- .observeOn(mainScheduler)
- .subscribe { boolean ->
- if (boolean) {
- handleBookmarksChange()
- }
- }
- }
-
- private fun putToolbarInRoot() {
- if (toolbar_layout.parent != ui_layout) {
- (toolbar_layout.parent as ViewGroup?)?.removeView(toolbar_layout)
-
- ui_layout.addView(toolbar_layout, 0)
- ui_layout.requestLayout()
- }
- setWebViewTranslation(0f)
- }
-
- private fun overlayToolbarOnWebView() {
- if (toolbar_layout.parent != content_frame) {
- (toolbar_layout.parent as ViewGroup?)?.removeView(toolbar_layout)
-
- content_frame.addView(toolbar_layout)
- content_frame.requestLayout()
- }
- setWebViewTranslation(toolbar_layout.height.toFloat())
- }
-
- private fun setWebViewTranslation(translation: Float) =
- if (isFullScreen) {
- currentTabView?.translationY = translation
- } else {
- currentTabView?.translationY = 0f
- }
-
- /**
- * method that shows a dialog asking what string the user wishes to search
- * for. It highlights the text entered.
- */
- private fun findInPage() = BrowserDialog.showEditText(
- this,
- R.string.action_find,
- R.string.search_hint,
- R.string.search_hint
- ) { text ->
- if (text.isNotEmpty()) {
- findResult = presenter?.findInPage(text)
- showFindInPageControls(text)
- }
- }
-
- private fun showFindInPageControls(text: String) {
- search_bar.visibility = VISIBLE
-
- findViewById(R.id.search_query).text = resources.getString(R.string.search_in_page_query, text)
- findViewById(R.id.button_next).setOnClickListener(this)
- findViewById(R.id.button_back).setOnClickListener(this)
- findViewById(R.id.button_quit).setOnClickListener(this)
- }
-
- override fun isColorMode(): Boolean = userPreferences.colorModeEnabled && !isDarkTheme
-
- override fun getTabModel(): TabsManager = tabsManager
-
- override fun showCloseDialog(position: Int) {
- if (position < 0) {
- return
- }
- BrowserDialog.show(this, R.string.dialog_title_close_browser,
- DialogItem(title = R.string.close_tab) {
- presenter?.deleteTab(position)
- },
- DialogItem(title = R.string.close_other_tabs) {
- presenter?.closeAllOtherTabs()
- },
- DialogItem(title = R.string.close_all_tabs, onClick = this::closeBrowser))
- }
-
- override fun notifyTabViewRemoved(position: Int) {
- logger.log(TAG, "Notify Tab Removed: $position")
- tabsView?.tabRemoved(position)
- }
-
- override fun notifyTabViewAdded() {
- logger.log(TAG, "Notify Tab Added")
- tabsView?.tabAdded()
- }
-
- override fun notifyTabViewChanged(position: Int) {
- logger.log(TAG, "Notify Tab Changed: $position")
- tabsView?.tabChanged(position)
- }
-
- override fun notifyTabViewInitialized() {
- logger.log(TAG, "Notify Tabs Initialized")
- tabsView?.tabsInitialized()
- }
-
- override fun updateSslState(sslState: SslState) {
- search_ssl_status.setImageDrawable(createSslDrawableForState(sslState))
-
- if (searchView?.hasFocus() == false) {
- search_ssl_status.updateVisibilityForContent()
- }
- }
-
- private fun ImageView.updateVisibilityForContent() {
- drawable?.let { visibility = VISIBLE } ?: run { visibility = GONE }
- }
-
- override fun tabChanged(tab: LightningView) {
- presenter?.tabChangeOccurred(tab)
- }
-
- override fun removeTabView() {
-
- logger.log(TAG, "Remove the tab view")
-
- currentTabView.removeFromParent()
-
- currentTabView = null
-
- // Use a delayed handler to make the transition smooth
- // otherwise it will get caught up with the showTab code
- // and cause a janky motion
- mainHandler.postDelayed(drawer_layout::closeDrawers, 200)
-
- }
-
- override fun setTabView(view: View) {
- if (currentTabView == view) {
- return
- }
-
- logger.log(TAG, "Setting the tab view")
-
- view.removeFromParent()
- currentTabView.removeFromParent()
-
- content_frame.addView(view, 0, MATCH_PARENT)
- if (isFullScreen) {
- view.translationY = toolbar_layout.height + toolbar_layout.translationY
- } else {
- view.translationY = 0f
- }
-
- view.requestFocus()
-
- currentTabView = view
-
- showActionBar()
-
- // Use a delayed handler to make the transition smooth
- // otherwise it will get caught up with the showTab code
- // and cause a janky motion
- mainHandler.postDelayed(drawer_layout::closeDrawers, 200)
- }
-
- override fun showBlockedLocalFileDialog(onPositiveClick: Function0) {
- AlertDialog.Builder(this)
- .setCancelable(true)
- .setTitle(R.string.title_warning)
- .setMessage(R.string.message_blocked_local)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.action_open) { _, _ -> onPositiveClick.invoke() }
- .resizeAndShow()
- }
-
- override fun showSnackbar(@StringRes resource: Int) = snackbar(resource)
-
- override fun tabCloseClicked(position: Int) {
- presenter?.deleteTab(position)
- }
-
- override fun tabClicked(position: Int) {
- presenter?.tabChanged(position)
- }
-
- override fun newTabButtonClicked() {
- presenter?.newTab(
- homePageInitializer,
- true
- )
- }
-
- override fun newTabButtonLongClicked() {
- presenter?.onNewTabLongClicked()
- }
-
- override fun bookmarkButtonClicked() {
- val currentTab = tabsManager.currentTab
- val url = currentTab?.url
- val title = currentTab?.title
- if (url == null || title == null) {
- return
- }
-
- if (!url.isSpecialUrl()) {
- bookmarkManager.isBookmark(url)
- .subscribeOn(databaseScheduler)
- .observeOn(mainScheduler)
- .subscribe { boolean ->
- if (boolean) {
- deleteBookmark(title, url)
- } else {
- addBookmark(title, url)
- }
- }
- }
- }
-
- override fun bookmarkItemClicked(entry: Bookmark.Entry) {
- presenter?.loadUrlInCurrentView(entry.url)
- // keep any jank from happening when the drawer is closed after the URL starts to load
- mainHandler.postDelayed({ closeDrawers(null) }, 150)
- }
-
- override fun handleHistoryChange() {
- historyPageFactory
- .buildPage()
- .subscribeOn(databaseScheduler)
- .observeOn(mainScheduler)
- .subscribeBy(onSuccess = { tabsManager.currentTab?.reload() })
- }
-
- protected fun handleNewIntent(intent: Intent) {
- presenter?.onNewIntent(intent)
- }
-
- protected fun performExitCleanUp() {
- exitCleanup.cleanUp(tabsManager.currentTab?.webView, this)
- }
-
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
-
- logger.log(TAG, "onConfigurationChanged")
-
- if (isFullScreen) {
- showActionBar()
- toolbar_layout.translationY = 0f
- setWebViewTranslation(toolbar_layout.height.toFloat())
- }
-
- invalidateOptionsMenu()
- initializeToolbarHeight(newConfig)
- }
-
- private fun initializeToolbarHeight(configuration: Configuration) =
- ui_layout.doOnLayout {
- // TODO externalize the dimensions
- val toolbarSize = if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
- R.dimen.toolbar_height_portrait
- } else {
- R.dimen.toolbar_height_landscape
- }
- toolbar.layoutParams = (toolbar.layoutParams as ConstraintLayout.LayoutParams).apply {
- height = dimen(toolbarSize)
- }
- toolbar.minimumHeight = toolbarSize
- toolbar.doOnLayout { setWebViewTranslation(toolbar_layout.height.toFloat()) }
- toolbar.requestLayout()
- }
-
- override fun closeBrowser() {
- currentTabView.removeFromParent()
- performExitCleanUp()
- val size = tabsManager.size()
- tabsManager.shutdown()
- currentTabView = null
- for (n in 0 until size) {
- tabsView?.tabRemoved(0)
- }
- finish()
- }
-
- override fun onBackPressed() {
- val currentTab = tabsManager.currentTab
- if (drawer_layout.isDrawerOpen(getTabDrawer())) {
- drawer_layout.closeDrawer(getTabDrawer())
- } else if (drawer_layout.isDrawerOpen(getBookmarkDrawer())) {
- bookmarksView?.navigateBack()
- } else {
- if (currentTab != null) {
- logger.log(TAG, "onBackPressed")
- if (searchView?.hasFocus() == true) {
- currentTab.requestFocus()
- } else if (currentTab.canGoBack()) {
- if (!currentTab.isShown) {
- onHideCustomView()
- } else {
- currentTab.goBack()
- }
- } else {
- if (customView != null || customViewCallback != null) {
- onHideCustomView()
- } else {
- presenter?.deleteTab(tabsManager.positionOf(currentTab))
- }
- }
- } else {
- logger.log(TAG, "This shouldn't happen ever")
- super.onBackPressed()
- }
- }
- }
-
- override fun onPause() {
- super.onPause()
- logger.log(TAG, "onPause")
- tabsManager.pauseAll()
-
- if (isIncognito() && isFinishing) {
- overridePendingTransition(R.anim.fade_in_scale, R.anim.slide_down_out)
- }
- }
-
- protected fun saveOpenTabs() {
- if (userPreferences.restoreLostTabsEnabled) {
- tabsManager.saveState()
- }
- }
-
- override fun onStop() {
- super.onStop()
- proxyUtils.onStop()
- }
-
- override fun onDestroy() {
- logger.log(TAG, "onDestroy")
-
- incognitoNotification?.hide()
-
- mainHandler.removeCallbacksAndMessages(null)
-
- presenter?.shutdown()
-
- super.onDestroy()
- }
-
- override fun onStart() {
- super.onStart()
- proxyUtils.onStart(this)
- }
-
- override fun onRestoreInstanceState(savedInstanceState: Bundle) {
- super.onRestoreInstanceState(savedInstanceState)
- tabsManager.shutdown()
- }
-
- override fun onResume() {
- super.onResume()
- logger.log(TAG, "onResume")
- if (swapBookmarksAndTabs != userPreferences.bookmarksAndTabsSwapped) {
- restart()
- }
-
- suggestionsAdapter?.let {
- it.refreshPreferences()
- it.refreshBookmarks()
- }
- tabsManager.resumeAll()
- initializePreferences()
-
- if (isFullScreen) {
- overlayToolbarOnWebView()
- } else {
- putToolbarInRoot()
- }
- }
-
- /**
- * searches the web for the query fixing any and all problems with the input
- * checks if it is a search, url, etc.
- */
- private fun searchTheWeb(query: String) {
- val currentTab = tabsManager.currentTab
- if (query.isEmpty()) {
- return
- }
- val searchUrl = "$searchText$QUERY_PLACE_HOLDER"
- if (currentTab != null) {
- currentTab.stopLoading()
- presenter?.loadUrlInCurrentView(smartUrlFilter(query.trim(), true, searchUrl))
- }
- }
-
- /**
- * Animates the color of the toolbar from one color to another. Optionally animates
- * the color of the tab background, for use when the tabs are displayed on the top
- * of the screen.
- *
- * @param favicon the Bitmap to extract the color from
- * @param tabBackground the optional LinearLayout to color
- */
- override fun changeToolbarBackground(favicon: Bitmap?, tabBackground: Drawable?) {
- if (!isColorMode()) {
- return
- }
- val defaultColor = ContextCompat.getColor(this, R.color.primary_color)
- if (currentUiColor == Color.BLACK) {
- currentUiColor = defaultColor
- }
- Palette.from(favicon ?: webPageBitmap!!).generate { palette ->
- // OR with opaque black to remove transparency glitches
- val color = Color.BLACK or (palette?.getVibrantColor(defaultColor) ?: defaultColor)
-
- // Lighten up the dark color if it is too dark
- val finalColor = if (!shouldShowTabsInDrawer || Utils.isColorTooDark(color)) {
- Utils.mixTwoColors(defaultColor, color, 0.25f)
- } else {
- color
- }
-
- val window = window
- if (!shouldShowTabsInDrawer) {
- window.setBackgroundDrawable(ColorDrawable(Color.BLACK))
- }
-
- val startSearchColor = getSearchBarColor(currentUiColor, defaultColor)
- val finalSearchColor = getSearchBarColor(finalColor, defaultColor)
-
- val animation = object : Animation() {
- override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
- val animatedColor = DrawableUtils.mixColor(interpolatedTime, currentUiColor, finalColor)
- if (shouldShowTabsInDrawer) {
- backgroundDrawable.color = animatedColor
- mainHandler.post { window.setBackgroundDrawable(backgroundDrawable) }
- } else {
- tabBackground?.tint(animatedColor)
- }
- currentUiColor = animatedColor
- toolbar_layout.setBackgroundColor(animatedColor)
- searchBackground?.background?.tint(
- DrawableUtils.mixColor(interpolatedTime, startSearchColor, finalSearchColor)
- )
- }
- }
- animation.duration = 300
- toolbar_layout.startAnimation(animation)
- }
- }
-
- private fun getSearchBarColor(requestedColor: Int, defaultColor: Int): Int =
- if (requestedColor == defaultColor) {
- if (isDarkTheme) DrawableUtils.mixColor(0.25f, defaultColor, Color.WHITE) else Color.WHITE
- } else {
- DrawableUtils.mixColor(0.25f, requestedColor, Color.WHITE)
- }
-
- @ColorInt
- override fun getUiColor(): Int = currentUiColor
-
- override fun updateUrl(url: String?, isLoading: Boolean) {
- if (url == null || searchView?.hasFocus() != false) {
- return
- }
- val currentTab = tabsManager.currentTab
- bookmarksView?.handleUpdatedUrl(url)
-
- val currentTitle = currentTab?.title
-
- searchView?.setText(searchBoxModel.getDisplayContent(url, currentTitle, isLoading))
- }
-
- override fun updateTabNumber(number: Int) {
- if (shouldShowTabsInDrawer && !isIncognito()) {
- tabCountView?.updateCount(number)
- }
- }
-
- override fun updateProgress(progress: Int) {
- setIsLoading(progress < 100)
- progress_view.progress = progress
- }
-
- protected fun addItemToHistory(title: String?, url: String) {
- if (url.isSpecialUrl()) {
- return
- }
-
- historyModel.visitHistoryEntry(url, title)
- .subscribeOn(databaseScheduler)
- .subscribe()
- }
-
- /**
- * method to generate search suggestions for the AutoCompleteTextView from
- * previously searched URLs
- */
- private fun initializeSearchSuggestions(getUrl: AutoCompleteTextView) {
- suggestionsAdapter = SuggestionsAdapter(this, isIncognito())
- suggestionsAdapter?.onSuggestionInsertClick = {
- if (it is SearchSuggestion) {
- getUrl.setText(it.title)
- getUrl.setSelection(it.title.length)
- } else {
- getUrl.setText(it.url)
- getUrl.setSelection(it.url.length)
- }
- }
- getUrl.onItemClickListener = OnItemClickListener { _, _, position, _ ->
- val url = when (val selection = suggestionsAdapter?.getItem(position) as WebPage) {
- is HistoryEntry,
- is Bookmark.Entry -> selection.url
- is SearchSuggestion -> selection.title
- else -> null
- } ?: return@OnItemClickListener
- getUrl.setText(url)
- searchTheWeb(url)
- inputMethodManager.hideSoftInputFromWindow(getUrl.windowToken, 0)
- presenter?.onAutoCompleteItemPressed()
- }
- getUrl.setAdapter(suggestionsAdapter)
- }
-
- /**
- * function that opens the HTML history page in the browser
- */
- private fun openHistory() {
- presenter?.newTab(
- historyPageInitializer,
- true
- )
- }
-
- private fun openDownloads() {
- presenter?.newTab(
- downloadPageInitializer,
- true
- )
- }
-
- /**
- * helper function that opens the bookmark drawer
- */
- private fun openBookmarks() {
- if (drawer_layout.isDrawerOpen(getTabDrawer())) {
- drawer_layout.closeDrawers()
- }
- drawer_layout.openDrawer(getBookmarkDrawer())
- }
-
- /**
- * This method closes any open drawer and executes the runnable after the drawers are closed.
- *
- * @param runnable an optional runnable to run after the drawers are closed.
- */
- protected fun closeDrawers(runnable: (() -> Unit)?) {
- if (!drawer_layout.isDrawerOpen(left_drawer) && !drawer_layout.isDrawerOpen(right_drawer)) {
- if (runnable != null) {
- runnable()
- return
- }
- }
- drawer_layout.closeDrawers()
-
- drawer_layout.addDrawerListener(object : DrawerLayout.DrawerListener {
- override fun onDrawerSlide(drawerView: View, slideOffset: Float) = Unit
-
- override fun onDrawerOpened(drawerView: View) = Unit
-
- override fun onDrawerClosed(drawerView: View) {
- runnable?.invoke()
- drawer_layout.removeDrawerListener(this)
- }
-
- override fun onDrawerStateChanged(newState: Int) = Unit
- })
- }
-
- override fun setForwardButtonEnabled(enabled: Boolean) {
- forwardMenuItem?.isEnabled = enabled
- tabsView?.setGoForwardEnabled(enabled)
- }
-
- override fun setBackButtonEnabled(enabled: Boolean) {
- backMenuItem?.isEnabled = enabled
- tabsView?.setGoBackEnabled(enabled)
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- backMenuItem = menu.findItem(R.id.action_back)
- forwardMenuItem = menu.findItem(R.id.action_forward)
- return super.onCreateOptionsMenu(menu)
- }
-
- /**
- * opens a file chooser
- * param ValueCallback is the message from the WebView indicating a file chooser
- * should be opened
- */
- override fun openFileChooser(uploadMsg: ValueCallback) {
- uploadMessageCallback = uploadMsg
- startActivityForResult(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "*/*"
- }, getString(R.string.title_file_chooser)), FILE_CHOOSER_REQUEST_CODE)
- }
-
- /**
- * used to allow uploading into the browser
- */
- override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
- if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
- val result = if (intent == null || resultCode != Activity.RESULT_OK) {
- null
- } else {
- intent.data
- }
-
- uploadMessageCallback?.onReceiveValue(result)
- uploadMessageCallback = null
- } else {
- val results: Array? = if (resultCode == Activity.RESULT_OK) {
- if (intent == null) {
- // If there is not data, then we may have taken a photo
- cameraPhotoPath?.let { arrayOf(it.toUri()) }
- } else {
- intent.dataString?.let { arrayOf(it.toUri()) }
- }
- } else {
- null
- }
-
- filePathCallback?.onReceiveValue(results)
- filePathCallback = null
- }
- } else {
- super.onActivityResult(requestCode, resultCode, intent)
- }
- }
-
- override fun showFileChooser(filePathCallback: ValueCallback>) {
- this.filePathCallback?.onReceiveValue(null)
- this.filePathCallback = filePathCallback
-
- // Create the File where the photo should go
- val intentArray: Array = try {
- arrayOf(Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
- putExtra("PhotoPath", cameraPhotoPath)
- putExtra(
- MediaStore.EXTRA_OUTPUT,
- Uri.fromFile(Utils.createImageFile().also { file ->
- cameraPhotoPath = "file:${file.absolutePath}"
- })
- )
- })
- } catch (ex: IOException) {
- // Error occurred while creating the File
- logger.log(TAG, "Unable to create Image File", ex)
- emptyArray()
- }
-
- startActivityForResult(Intent(Intent.ACTION_CHOOSER).apply {
- putExtra(Intent.EXTRA_INTENT, Intent(Intent.ACTION_GET_CONTENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "*/*"
- })
- putExtra(Intent.EXTRA_TITLE, "Image Chooser")
- putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray)
- }, FILE_CHOOSER_REQUEST_CODE)
- }
-
- override fun onShowCustomView(view: View, callback: CustomViewCallback, requestedOrientation: Int) {
- val currentTab = tabsManager.currentTab
- if (customView != null) {
- try {
- callback.onCustomViewHidden()
- } catch (e: Exception) {
- logger.log(TAG, "Error hiding custom view", e)
- }
-
- return
- }
-
- try {
- view.keepScreenOn = true
- } catch (e: SecurityException) {
- logger.log(TAG, "WebView is not allowed to keep the screen on")
- }
-
- originalOrientation = getRequestedOrientation()
- customViewCallback = callback
- customView = view
-
- setRequestedOrientation(requestedOrientation)
- val decorView = window.decorView as FrameLayout
-
- fullscreenContainerView = FrameLayout(this)
- fullscreenContainerView?.setBackgroundColor(ContextCompat.getColor(this, R.color.black))
- if (view is FrameLayout) {
- val child = view.focusedChild
- if (child is VideoView) {
- videoView = child
- child.setOnErrorListener(VideoCompletionListener())
- child.setOnCompletionListener(VideoCompletionListener())
- }
- } else if (view is VideoView) {
- videoView = view
- view.setOnErrorListener(VideoCompletionListener())
- view.setOnCompletionListener(VideoCompletionListener())
- }
- decorView.addView(fullscreenContainerView, COVER_SCREEN_PARAMS)
- fullscreenContainerView?.addView(customView, COVER_SCREEN_PARAMS)
- decorView.requestLayout()
- setFullscreen(enabled = true, immersive = true)
- currentTab?.setVisibility(INVISIBLE)
- }
-
- override fun onHideCustomView() {
- val currentTab = tabsManager.currentTab
- if (customView == null || customViewCallback == null || currentTab == null) {
- if (customViewCallback != null) {
- try {
- customViewCallback?.onCustomViewHidden()
- } catch (e: Exception) {
- logger.log(TAG, "Error hiding custom view", e)
- }
-
- customViewCallback = null
- }
- return
- }
- logger.log(TAG, "onHideCustomView")
- currentTab.setVisibility(VISIBLE)
- try {
- customView?.keepScreenOn = false
- } catch (e: SecurityException) {
- logger.log(TAG, "WebView is not allowed to keep the screen on")
- }
-
- setFullscreen(userPreferences.hideStatusBarEnabled, false)
- if (fullscreenContainerView != null) {
- val parent = fullscreenContainerView?.parent as ViewGroup
- parent.removeView(fullscreenContainerView)
- fullscreenContainerView?.removeAllViews()
- }
-
- fullscreenContainerView = null
- customView = null
-
- logger.log(TAG, "VideoView is being stopped")
- videoView?.stopPlayback()
- videoView?.setOnErrorListener(null)
- videoView?.setOnCompletionListener(null)
- videoView = null
-
- try {
- customViewCallback?.onCustomViewHidden()
- } catch (e: Exception) {
- logger.log(TAG, "Error hiding custom view", e)
- }
-
- customViewCallback = null
- requestedOrientation = originalOrientation
- }
-
- private inner class VideoCompletionListener : MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
-
- override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean = false
-
- override fun onCompletion(mp: MediaPlayer) = onHideCustomView()
-
- }
-
- override fun onWindowFocusChanged(hasFocus: Boolean) {
- super.onWindowFocusChanged(hasFocus)
- logger.log(TAG, "onWindowFocusChanged")
- if (hasFocus) {
- setFullscreen(hideStatusBar, isImmersiveMode)
- }
- }
-
- override fun onBackButtonPressed() {
- if (drawer_layout.closeDrawerIfOpen(getTabDrawer())) {
- val currentTab = tabsManager.currentTab
- if (currentTab?.canGoBack() == true) {
- currentTab.goBack()
- } else if (currentTab != null) {
- tabsManager.let { presenter?.deleteTab(it.positionOf(currentTab)) }
- }
- } else if (drawer_layout.closeDrawerIfOpen(getBookmarkDrawer())) {
- // Don't do anything other than close the bookmarks drawer when the activity is being
- // delegated to.
- }
- }
-
- override fun onForwardButtonPressed() {
- val currentTab = tabsManager.currentTab
- if (currentTab?.canGoForward() == true) {
- currentTab.goForward()
- closeDrawers(null)
- }
- }
-
- override fun onHomeButtonPressed() {
- tabsManager.currentTab?.loadHomePage()
- closeDrawers(null)
- }
-
- /**
- * This method sets whether or not the activity will display
- * in full-screen mode (i.e. the ActionBar will be hidden) and
- * whether or not immersive mode should be set. This is used to
- * set both parameters correctly as during a full-screen video,
- * both need to be set, but other-wise we leave it up to user
- * preference.
- *
- * @param enabled true to enable full-screen, false otherwise
- * @param immersive true to enable immersive mode, false otherwise
- */
- private fun setFullscreen(enabled: Boolean, immersive: Boolean) {
- hideStatusBar = enabled
- isImmersiveMode = immersive
- val window = window
- val decor = window.decorView
- if (enabled) {
- if (immersive) {
- decor.systemUiVisibility = (SYSTEM_UI_FLAG_LAYOUT_STABLE
- or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- or SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- or SYSTEM_UI_FLAG_HIDE_NAVIGATION
- or SYSTEM_UI_FLAG_FULLSCREEN
- or SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
- } else {
- decor.systemUiVisibility = SYSTEM_UI_FLAG_VISIBLE
- }
- window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN)
- } else {
- window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
- decor.systemUiVisibility = SYSTEM_UI_FLAG_VISIBLE
- }
- }
-
- /**
- * This method handles the JavaScript callback to create a new tab.
- * Basically this handles the event that JavaScript needs to create
- * a popup.
- *
- * @param resultMsg the transport message used to send the URL to
- * the newly created WebView.
- */
- override fun onCreateWindow(resultMsg: Message) {
- presenter?.newTab(ResultMessageInitializer(resultMsg), true)
- }
-
- /**
- * Closes the specified [LightningView]. This implements
- * the JavaScript callback that asks the tab to close itself and
- * is especially helpful when a page creates a redirect and does
- * not need the tab to stay open any longer.
- *
- * @param tab the LightningView to close, delete it.
- */
- override fun onCloseWindow(tab: LightningView) {
- presenter?.deleteTab(tabsManager.positionOf(tab))
- }
-
- /**
- * Hide the ActionBar using an animation if we are in full-screen
- * mode. This method also re-parents the ActionBar if its parent is
- * incorrect so that the animation can happen correctly.
- */
- override fun hideActionBar() {
- if (isFullScreen) {
- if (toolbar_layout == null || content_frame == null)
- return
-
- val height = toolbar_layout.height
- if (toolbar_layout.translationY > -0.01f) {
- val hideAnimation = object : Animation() {
- override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
- val trans = interpolatedTime * height
- toolbar_layout.translationY = -trans
- setWebViewTranslation(height - trans)
- }
- }
- hideAnimation.duration = 250
- hideAnimation.interpolator = BezierDecelerateInterpolator()
- content_frame.startAnimation(hideAnimation)
- }
- }
- }
-
- /**
- * Display the ActionBar using an animation if we are in full-screen
- * mode. This method also re-parents the ActionBar if its parent is
- * incorrect so that the animation can happen correctly.
- */
- override fun showActionBar() {
- if (isFullScreen) {
- logger.log(TAG, "showActionBar")
- if (toolbar_layout == null)
- return
-
- var height = toolbar_layout.height
- if (height == 0) {
- toolbar_layout.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
- height = toolbar_layout.measuredHeight
- }
-
- val totalHeight = height
- if (toolbar_layout.translationY < -(height - 0.01f)) {
- val show = object : Animation() {
- override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
- val trans = interpolatedTime * totalHeight
- toolbar_layout.translationY = trans - totalHeight
- setWebViewTranslation(trans)
- }
- }
- show.duration = 250
- show.interpolator = BezierDecelerateInterpolator()
- content_frame.startAnimation(show)
- }
- }
- }
-
- override fun handleBookmarksChange() {
- val currentTab = tabsManager.currentTab
- if (currentTab != null && currentTab.url.isBookmarkUrl()) {
- currentTab.loadBookmarkPage()
- }
- if (currentTab != null) {
- bookmarksView?.handleUpdatedUrl(currentTab.url)
- }
- suggestionsAdapter?.refreshBookmarks()
- }
-
- override fun handleDownloadDeleted() {
- val currentTab = tabsManager.currentTab
- if (currentTab != null && currentTab.url.isDownloadsUrl()) {
- currentTab.loadDownloadsPage()
- }
- if (currentTab != null) {
- bookmarksView?.handleUpdatedUrl(currentTab.url)
- }
- }
-
- override fun handleBookmarkDeleted(bookmark: Bookmark) {
- bookmarksView?.handleBookmarkDeleted(bookmark)
- handleBookmarksChange()
- }
-
- override fun handleNewTab(newTabType: LightningDialogBuilder.NewTab, url: String) {
- val urlInitializer = UrlInitializer(url)
- when (newTabType) {
- LightningDialogBuilder.NewTab.FOREGROUND -> presenter?.newTab(urlInitializer, true)
- LightningDialogBuilder.NewTab.BACKGROUND -> presenter?.newTab(urlInitializer, false)
- LightningDialogBuilder.NewTab.INCOGNITO -> {
- drawer_layout.closeDrawers()
- val intent = IncognitoActivity.createIntent(this, url.toUri())
- startActivity(intent)
- overridePendingTransition(R.anim.slide_up_in, R.anim.fade_out_scale)
- }
- }
- }
-
- /**
- * This method lets the search bar know that the page is currently loading
- * and that it should display the stop icon to indicate to the user that
- * pressing it stops the page from loading
- */
- private fun setIsLoading(isLoading: Boolean) {
- if (searchView?.hasFocus() == false) {
- search_ssl_status.updateVisibilityForContent()
- search_refresh.setImageResource(if (isLoading) R.drawable.ic_action_delete else R.drawable.ic_action_refresh)
- }
- }
-
- /**
- * handle presses on the refresh icon in the search bar, if the page is
- * loading, stop the page, if it is done loading refresh the page.
- * See setIsFinishedLoading and setIsLoading for displaying the correct icon
- */
- private fun refreshOrStop() {
- val currentTab = tabsManager.currentTab
- if (currentTab != null) {
- if (currentTab.progress < 100) {
- currentTab.stopLoading()
- } else {
- currentTab.reload()
- }
- }
- }
-
- /**
- * Handle the click event for the views that are using
- * this class as a click listener. This method should
- * distinguish between the various views using their IDs.
- *
- * @param v the view that the user has clicked
- */
- override fun onClick(v: View) {
- val currentTab = tabsManager.currentTab ?: return
- when (v.id) {
- R.id.home_button -> when {
- searchView?.hasFocus() == true -> currentTab.requestFocus()
- shouldShowTabsInDrawer -> drawer_layout.openDrawer(getTabDrawer())
- else -> currentTab.loadHomePage()
- }
- R.id.button_next -> findResult?.nextResult()
- R.id.button_back -> findResult?.previousResult()
- R.id.button_quit -> {
- findResult?.clearResults()
- findResult = null
- search_bar.visibility = GONE
- }
- }
- }
-
- /**
- * Handle the callback that permissions requested have been granted or not.
- * This method should act upon the results of the permissions request.
- *
- * @param requestCode the request code sent when initially making the request
- * @param permissions the array of the permissions that was requested
- * @param grantResults the results of the permissions requests that provides
- * information on whether the request was granted or not
- */
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
- PermissionsManager.getInstance().notifyPermissionsChange(permissions, grantResults)
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- }
-
- /**
- * If the [drawer] is open, close it and return true. Return false otherwise.
- */
- private fun DrawerLayout.closeDrawerIfOpen(drawer: View): Boolean =
- if (isDrawerOpen(drawer)) {
- closeDrawer(drawer)
- true
- } else {
- false
- }
-
- companion object {
-
- private const val TAG = "BrowserActivity"
-
- const val INTENT_PANIC_TRIGGER = "info.guardianproject.panic.action.TRIGGER"
-
- private const val FILE_CHOOSER_REQUEST_CODE = 1111
-
- // Constant
- private val MATCH_PARENT = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
- private val COVER_SCREEN_PARAMS = FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
-
- }
-
-}
diff --git a/app/src/main/java/acr/browser/lightning/browser/bookmark/BookmarkRecyclerViewAdapter.kt b/app/src/main/java/acr/browser/lightning/browser/bookmark/BookmarkRecyclerViewAdapter.kt
new file mode 100644
index 000000000..39cfa7030
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/bookmark/BookmarkRecyclerViewAdapter.kt
@@ -0,0 +1,48 @@
+package acr.browser.lightning.browser.bookmark
+
+import acr.browser.lightning.R
+import acr.browser.lightning.browser.image.ImageLoader
+import acr.browser.lightning.database.Bookmark
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+
+/**
+ * An adapter that creates the views for bookmark list items and binds the bookmark data to them.
+ *
+ * @param onClick Invoked when the cell is clicked.
+ * @param onLongClick Invoked when the cell is long pressed.
+ * @param imageLoader The image loader needed to load favicons.
+ */
+class BookmarkRecyclerViewAdapter(
+ private val onClick: (Int) -> Unit,
+ private val onLongClick: (Int) -> Unit,
+ private val imageLoader: ImageLoader
+) : ListAdapter(
+ object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean =
+ oldItem == newItem
+
+ override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean =
+ oldItem == newItem
+ }
+) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ val itemView = inflater.inflate(R.layout.bookmark_list_item, parent, false)
+
+ return BookmarkViewHolder(
+ itemView,
+ onItemLongClickListener = onLongClick,
+ onItemClickListener = onClick
+ )
+ }
+
+ override fun onBindViewHolder(holder: BookmarkViewHolder, position: Int) {
+ val viewModel = getItem(position)
+ holder.binding.textBookmark.text = viewModel.title
+
+ imageLoader.loadImage(holder.binding.faviconBookmark, viewModel)
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/bookmark/BookmarkViewHolder.kt b/app/src/main/java/acr/browser/lightning/browser/bookmark/BookmarkViewHolder.kt
new file mode 100644
index 000000000..2bab65886
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/bookmark/BookmarkViewHolder.kt
@@ -0,0 +1,34 @@
+package acr.browser.lightning.browser.bookmark
+
+import acr.browser.lightning.databinding.BookmarkListItemBinding
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * The view holder that shows bookmark items.
+ *
+ * @param onItemClickListener Invoked when the cell is clicked.
+ * @param onItemLongClickListener Invoked when the cell is long pressed.
+ */
+class BookmarkViewHolder(
+ itemView: View,
+ private val onItemLongClickListener: (Int) -> Unit,
+ private val onItemClickListener: (Int) -> Unit
+) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
+
+ val binding = BookmarkListItemBinding.bind(itemView)
+
+ init {
+ itemView.setOnLongClickListener(this)
+ itemView.setOnClickListener(this)
+ }
+
+ override fun onClick(v: View) {
+ onItemClickListener(adapterPosition)
+ }
+
+ override fun onLongClick(v: View): Boolean {
+ onItemLongClickListener(adapterPosition)
+ return true
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarkUiModel.kt b/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarkUiModel.kt
deleted file mode 100644
index e079981b6..000000000
--- a/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarkUiModel.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package acr.browser.lightning.browser.bookmarks
-
-import acr.browser.lightning.browser.BookmarksView
-
-/**
- * The UI model representing the current folder shown by the [BookmarksView].
- *
- * Created by anthonycr on 5/7/17.
- */
-class BookmarkUiModel {
-
- /**
- * Sets the current folder that is being shown. Null represents the root folder.
- */
- var currentFolder: String? = null
-
- /**
- * Determines if the current folder is the root folder.
- *
- * @return true if the current folder is the root, false otherwise.
- */
- fun isCurrentFolderRoot(): Boolean = currentFolder == null
-
-}
diff --git a/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarksDrawerView.kt b/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarksDrawerView.kt
deleted file mode 100644
index ac14cbfc7..000000000
--- a/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarksDrawerView.kt
+++ /dev/null
@@ -1,377 +0,0 @@
-package acr.browser.lightning.browser.bookmarks
-
-import acr.browser.lightning.R
-import acr.browser.lightning.adblock.allowlist.AllowListModel
-import acr.browser.lightning.animation.AnimationUtils
-import acr.browser.lightning.browser.BookmarksView
-import acr.browser.lightning.browser.TabsManager
-import acr.browser.lightning.controller.UIController
-import acr.browser.lightning.database.Bookmark
-import acr.browser.lightning.database.bookmark.BookmarkRepository
-import acr.browser.lightning.di.DatabaseScheduler
-import acr.browser.lightning.di.MainScheduler
-import acr.browser.lightning.di.NetworkScheduler
-import acr.browser.lightning.di.injector
-import acr.browser.lightning.dialog.BrowserDialog
-import acr.browser.lightning.dialog.DialogItem
-import acr.browser.lightning.dialog.LightningDialogBuilder
-import acr.browser.lightning.extensions.color
-import acr.browser.lightning.extensions.drawable
-import acr.browser.lightning.extensions.inflater
-import acr.browser.lightning.favicon.FaviconModel
-import acr.browser.lightning.reading.activity.ReadingActivity
-import acr.browser.lightning.utils.isSpecialUrl
-import android.app.Activity
-import android.content.Context
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import io.reactivex.Scheduler
-import io.reactivex.Single
-import io.reactivex.disposables.Disposable
-import io.reactivex.rxkotlin.subscribeBy
-import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
-
-/**
- * The view that displays bookmarks in a list and some controls.
- */
-class BookmarksDrawerView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : LinearLayout(context, attrs, defStyleAttr), BookmarksView {
-
- @Inject internal lateinit var bookmarkModel: BookmarkRepository
- @Inject internal lateinit var allowListModel: AllowListModel
- @Inject internal lateinit var bookmarksDialogBuilder: LightningDialogBuilder
- @Inject internal lateinit var faviconModel: FaviconModel
- @Inject @field:DatabaseScheduler internal lateinit var databaseScheduler: Scheduler
- @Inject @field:NetworkScheduler internal lateinit var networkScheduler: Scheduler
- @Inject @field:MainScheduler internal lateinit var mainScheduler: Scheduler
-
- private val uiController: UIController
-
- // Adapter
- private var bookmarkAdapter: BookmarkListAdapter? = null
-
- // Colors
- private var scrollIndex: Int = 0
-
- private var bookmarksSubscription: Disposable? = null
- private var bookmarkUpdateSubscription: Disposable? = null
-
- private val uiModel = BookmarkUiModel()
-
- private var bookmarkRecyclerView: RecyclerView? = null
- private var backNavigationView: ImageView? = null
- private var addBookmarkView: ImageView? = null
-
- init {
- context.inflater.inflate(R.layout.bookmark_drawer, this, true)
- context.injector.inject(this)
-
- uiController = context as UIController
-
- bookmarkRecyclerView = findViewById(R.id.bookmark_list_view)
- backNavigationView = findViewById(R.id.bookmark_back_button)
- addBookmarkView = findViewById(R.id.action_add_bookmark)
- backNavigationView?.setOnClickListener {
- if (!uiModel.isCurrentFolderRoot()) {
- setBookmarksShown(null, true)
- bookmarkRecyclerView?.layoutManager?.scrollToPosition(scrollIndex)
- }
- }
- addBookmarkView?.setOnClickListener { uiController.bookmarkButtonClicked() }
- findViewById(R.id.action_reading).setOnClickListener {
- getTabsManager().currentTab?.url?.let {
- ReadingActivity.launch(context, it)
- }
- }
- findViewById(R.id.action_page_tools).setOnClickListener { showPageToolsDialog(context) }
-
- bookmarkAdapter = BookmarkListAdapter(
- context,
- faviconModel,
- networkScheduler,
- mainScheduler,
- ::handleItemLongPress,
- ::handleItemClick
- )
-
- bookmarkRecyclerView?.let {
- it.layoutManager = LinearLayoutManager(context)
- it.adapter = bookmarkAdapter
- }
-
- setBookmarksShown(null, true)
- }
-
- override fun onDetachedFromWindow() {
- super.onDetachedFromWindow()
-
- bookmarksSubscription?.dispose()
- bookmarkUpdateSubscription?.dispose()
-
- bookmarkAdapter?.cleanupSubscriptions()
- }
-
- private fun getTabsManager(): TabsManager = uiController.getTabModel()
-
- private fun updateBookmarkIndicator(url: String) {
- bookmarkUpdateSubscription?.dispose()
- bookmarkUpdateSubscription = bookmarkModel.isBookmark(url)
- .subscribeOn(databaseScheduler)
- .observeOn(mainScheduler)
- .subscribe { isBookmark ->
- bookmarkUpdateSubscription = null
- addBookmarkView?.isSelected = isBookmark
- addBookmarkView?.isEnabled = !url.isSpecialUrl()
- }
- }
-
- override fun handleBookmarkDeleted(bookmark: Bookmark) = when (bookmark) {
- is Bookmark.Folder -> setBookmarksShown(null, false)
- is Bookmark.Entry -> bookmarkAdapter?.deleteItem(BookmarksViewModel(bookmark)) ?: Unit
- }
-
- private fun setBookmarksShown(folder: String?, animate: Boolean) {
- bookmarksSubscription?.dispose()
- bookmarksSubscription = bookmarkModel.getBookmarksFromFolderSorted(folder)
- .concatWith(Single.defer {
- if (folder == null) {
- bookmarkModel.getFoldersSorted()
- } else {
- Single.just(emptyList())
- }
- })
- .toList()
- .map { it.flatten() }
- .subscribeOn(databaseScheduler)
- .observeOn(mainScheduler)
- .subscribe { bookmarksAndFolders ->
- uiModel.currentFolder = folder
- setBookmarkDataSet(bookmarksAndFolders, animate)
- }
- }
-
- private fun setBookmarkDataSet(items: List, animate: Boolean) {
- bookmarkAdapter?.updateItems(items.map { BookmarksViewModel(it) })
- val resource = if (uiModel.isCurrentFolderRoot()) {
- R.drawable.ic_action_star
- } else {
- R.drawable.ic_action_back
- }
-
- if (animate) {
- backNavigationView?.let {
- val transition = AnimationUtils.createRotationTransitionAnimation(it, resource)
- it.startAnimation(transition)
- }
- } else {
- backNavigationView?.setImageResource(resource)
- }
- }
-
- private fun handleItemLongPress(bookmark: Bookmark): Boolean {
- (context as Activity?)?.let {
- when (bookmark) {
- is Bookmark.Folder -> bookmarksDialogBuilder.showBookmarkFolderLongPressedDialog(it, uiController, bookmark)
- is Bookmark.Entry -> bookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(it, uiController, bookmark)
- }
- }
- return true
- }
-
- private fun handleItemClick(bookmark: Bookmark) = when (bookmark) {
- is Bookmark.Folder -> {
- scrollIndex = (bookmarkRecyclerView?.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
- setBookmarksShown(bookmark.title, true)
- }
- is Bookmark.Entry -> uiController.bookmarkItemClicked(bookmark)
- }
-
-
- /**
- * Show the page tools dialog.
- */
- private fun showPageToolsDialog(context: Context) {
- val currentTab = getTabsManager().currentTab ?: return
- val isAllowedAds = allowListModel.isUrlAllowedAds(currentTab.url)
- val whitelistString = if (isAllowedAds) {
- R.string.dialog_adblock_enable_for_site
- } else {
- R.string.dialog_adblock_disable_for_site
- }
-
- BrowserDialog.showWithIcons(context, context.getString(R.string.dialog_tools_title),
- DialogItem(
- icon = context.drawable(R.drawable.ic_action_desktop),
- title = R.string.dialog_toggle_desktop
- ) {
- getTabsManager().currentTab?.apply {
- toggleDesktopUA()
- reload()
- // TODO add back drawer closing
- }
- },
- DialogItem(
- icon = context.drawable(R.drawable.ic_block),
- colorTint = context.color(R.color.error_red).takeIf { isAllowedAds },
- title = whitelistString,
- isConditionMet = !currentTab.url.isSpecialUrl()
- ) {
- if (isAllowedAds) {
- allowListModel.removeUrlFromAllowList(currentTab.url)
- } else {
- allowListModel.addUrlToAllowList(currentTab.url)
- }
- getTabsManager().currentTab?.reload()
- }
- )
- }
-
- override fun navigateBack() {
- if (uiModel.isCurrentFolderRoot()) {
- uiController.onBackButtonPressed()
- } else {
- setBookmarksShown(null, true)
- bookmarkRecyclerView?.layoutManager?.scrollToPosition(scrollIndex)
- }
- }
-
- override fun handleUpdatedUrl(url: String) {
- updateBookmarkIndicator(url)
- val folder = uiModel.currentFolder
- setBookmarksShown(folder, false)
- }
-
- private class BookmarkViewHolder(
- itemView: View,
- private val adapter: BookmarkListAdapter,
- private val onItemLongClickListener: (Bookmark) -> Boolean,
- private val onItemClickListener: (Bookmark) -> Unit
- ) : RecyclerView.ViewHolder(itemView), OnClickListener, OnLongClickListener {
-
- val txtTitle: TextView = itemView.findViewById(R.id.textBookmark)
- val favicon: ImageView = itemView.findViewById(R.id.faviconBookmark)
-
- init {
- itemView.setOnLongClickListener(this)
- itemView.setOnClickListener(this)
- }
-
- override fun onClick(v: View) {
- val index = adapterPosition
- if (index.toLong() != RecyclerView.NO_ID) {
- onItemClickListener(adapter.itemAt(index).bookmark)
- }
- }
-
- override fun onLongClick(v: View): Boolean {
- val index = adapterPosition
- return index != RecyclerView.NO_POSITION && onItemLongClickListener(adapter.itemAt(index).bookmark)
- }
- }
-
- private class BookmarkListAdapter(
- context: Context,
- private val faviconModel: FaviconModel,
- private val networkScheduler: Scheduler,
- private val mainScheduler: Scheduler,
- private val onItemLongClickListener: (Bookmark) -> Boolean,
- private val onItemClickListener: (Bookmark) -> Unit
- ) : RecyclerView.Adapter() {
-
- private var bookmarks: List = listOf()
- private val faviconFetchSubscriptions = ConcurrentHashMap()
- private val folderIcon = context.drawable(R.drawable.ic_folder)
- private val webpageIcon = context.drawable(R.drawable.ic_webpage)
-
- fun itemAt(position: Int): BookmarksViewModel = bookmarks[position]
-
- fun deleteItem(item: BookmarksViewModel) {
- val newList = bookmarks - item
- updateItems(newList)
- }
-
- fun updateItems(newList: List) {
- val oldList = bookmarks
- bookmarks = newList
-
- val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
- override fun getOldListSize() = oldList.size
-
- override fun getNewListSize() = bookmarks.size
-
- override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
- oldList[oldItemPosition].bookmark.url == bookmarks[newItemPosition].bookmark.url
-
- override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
- oldList[oldItemPosition] == bookmarks[newItemPosition]
- })
-
- diffResult.dispatchUpdatesTo(this)
- }
-
- fun cleanupSubscriptions() {
- for (subscription in faviconFetchSubscriptions.values) {
- subscription.dispose()
- }
- faviconFetchSubscriptions.clear()
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkViewHolder {
- val inflater = LayoutInflater.from(parent.context)
- val itemView = inflater.inflate(R.layout.bookmark_list_item, parent, false)
-
- return BookmarkViewHolder(itemView, this, onItemLongClickListener, onItemClickListener)
- }
-
- override fun onBindViewHolder(holder: BookmarkViewHolder, position: Int) {
- holder.itemView.jumpDrawablesToCurrentState()
-
- val viewModel = bookmarks[position]
- holder.txtTitle.text = viewModel.bookmark.title
-
- val url = viewModel.bookmark.url
- holder.favicon.tag = url
-
- viewModel.icon?.let {
- holder.favicon.setImageBitmap(it)
- return
- }
-
- val imageDrawable = when (viewModel.bookmark) {
- is Bookmark.Folder -> folderIcon
- is Bookmark.Entry -> webpageIcon.also {
- faviconFetchSubscriptions[url]?.dispose()
- faviconFetchSubscriptions[url] = faviconModel
- .faviconForUrl(url, viewModel.bookmark.title)
- .subscribeOn(networkScheduler)
- .observeOn(mainScheduler)
- .subscribeBy(
- onSuccess = { bitmap ->
- viewModel.icon = bitmap
- if (holder.favicon.tag == url) {
- holder.favicon.setImageBitmap(bitmap)
- }
- }
- )
- }
- }
-
- holder.favicon.setImageDrawable(imageDrawable)
- }
-
- override fun getItemCount() = bookmarks.size
- }
-
-}
diff --git a/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarksViewModel.kt b/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarksViewModel.kt
deleted file mode 100644
index 402c1a065..000000000
--- a/app/src/main/java/acr/browser/lightning/browser/bookmarks/BookmarksViewModel.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package acr.browser.lightning.browser.bookmarks
-
-import acr.browser.lightning.database.Bookmark
-import android.graphics.Bitmap
-
-/**
- * The data model representing a [Bookmark] in a list.
- *
- * @param bookmark The bookmark backing this view model, either an entry or a folder.
- * @param icon The icon for this bookmark.
- */
-data class BookmarksViewModel(
- val bookmark: Bookmark,
- var icon: Bitmap? = null
-)
diff --git a/app/src/main/java/acr/browser/lightning/browser/cleanup/BasicIncognitoExitCleanup.kt b/app/src/main/java/acr/browser/lightning/browser/cleanup/BasicIncognitoExitCleanup.kt
index d3e7a474b..0c32679fb 100644
--- a/app/src/main/java/acr/browser/lightning/browser/cleanup/BasicIncognitoExitCleanup.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/BasicIncognitoExitCleanup.kt
@@ -1,8 +1,6 @@
package acr.browser.lightning.browser.cleanup
-import acr.browser.lightning.browser.activity.BrowserActivity
import acr.browser.lightning.utils.WebUtils
-import android.webkit.WebView
import javax.inject.Inject
/**
@@ -10,7 +8,7 @@ import javax.inject.Inject
* significantly less secure than on API > 28 since we can separate WebView data from
*/
class BasicIncognitoExitCleanup @Inject constructor() : ExitCleanup {
- override fun cleanUp(webView: WebView?, context: BrowserActivity) {
+ override fun cleanUp() {
// We want to make sure incognito mode is secure as possible without also breaking existing
// browser instances.
WebUtils.clearWebStorage()
diff --git a/app/src/main/java/acr/browser/lightning/browser/cleanup/DelegatingExitCleanup.kt b/app/src/main/java/acr/browser/lightning/browser/cleanup/DelegatingExitCleanup.kt
index 56bf8b76a..24b962112 100644
--- a/app/src/main/java/acr/browser/lightning/browser/cleanup/DelegatingExitCleanup.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/DelegatingExitCleanup.kt
@@ -1,9 +1,10 @@
package acr.browser.lightning.browser.cleanup
import acr.browser.lightning.Capabilities
-import acr.browser.lightning.MainActivity
-import acr.browser.lightning.browser.activity.BrowserActivity
+import acr.browser.lightning.browser.BrowserActivity
+import acr.browser.lightning.DefaultBrowserActivity
import acr.browser.lightning.isSupported
+import android.app.Activity
import android.webkit.WebView
import javax.inject.Inject
@@ -14,13 +15,14 @@ import javax.inject.Inject
class DelegatingExitCleanup @Inject constructor(
private val basicIncognitoExitCleanup: BasicIncognitoExitCleanup,
private val enhancedIncognitoExitCleanup: EnhancedIncognitoExitCleanup,
- private val normalExitCleanup: NormalExitCleanup
+ private val normalExitCleanup: NormalExitCleanup,
+ private val activity: Activity
) : ExitCleanup {
- override fun cleanUp(webView: WebView?, context: BrowserActivity) {
+ override fun cleanUp() {
when {
- context is MainActivity -> normalExitCleanup.cleanUp(webView, context)
- Capabilities.FULL_INCOGNITO.isSupported -> enhancedIncognitoExitCleanup.cleanUp(webView, context)
- else -> basicIncognitoExitCleanup.cleanUp(webView, context)
+ activity is DefaultBrowserActivity -> normalExitCleanup.cleanUp()
+ Capabilities.FULL_INCOGNITO.isSupported -> enhancedIncognitoExitCleanup.cleanUp()
+ else -> basicIncognitoExitCleanup.cleanUp()
}
}
}
diff --git a/app/src/main/java/acr/browser/lightning/browser/cleanup/EnhancedIncognitoExitCleanup.kt b/app/src/main/java/acr/browser/lightning/browser/cleanup/EnhancedIncognitoExitCleanup.kt
index eefd6c07e..75e3a3c6d 100644
--- a/app/src/main/java/acr/browser/lightning/browser/cleanup/EnhancedIncognitoExitCleanup.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/EnhancedIncognitoExitCleanup.kt
@@ -1,9 +1,8 @@
package acr.browser.lightning.browser.cleanup
-import acr.browser.lightning.browser.activity.BrowserActivity
import acr.browser.lightning.log.Logger
import acr.browser.lightning.utils.WebUtils
-import android.webkit.WebView
+import android.app.Activity
import javax.inject.Inject
/**
@@ -11,12 +10,13 @@ import javax.inject.Inject
* clears cookies and all web data, which can be done without affecting
*/
class EnhancedIncognitoExitCleanup @Inject constructor(
- private val logger: Logger
+ private val logger: Logger,
+ private val activity: Activity
) : ExitCleanup {
- override fun cleanUp(webView: WebView?, context: BrowserActivity) {
- WebUtils.clearCache(webView)
+ override fun cleanUp() {
+ WebUtils.clearCache(activity)
logger.log(TAG, "Cache Cleared")
- WebUtils.clearCookies(context)
+ WebUtils.clearCookies()
logger.log(TAG, "Cookies Cleared")
WebUtils.clearWebStorage()
logger.log(TAG, "WebStorage Cleared")
diff --git a/app/src/main/java/acr/browser/lightning/browser/cleanup/ExitCleanup.kt b/app/src/main/java/acr/browser/lightning/browser/cleanup/ExitCleanup.kt
index 796fa2f28..4a0d68925 100644
--- a/app/src/main/java/acr/browser/lightning/browser/cleanup/ExitCleanup.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/ExitCleanup.kt
@@ -1,8 +1,5 @@
package acr.browser.lightning.browser.cleanup
-import acr.browser.lightning.browser.activity.BrowserActivity
-import android.webkit.WebView
-
/**
* A command that runs as the browser instance is shutting down to clean up anything that needs to
* be cleaned up. For instance, if the user has chosen to clear cache on exit or if incognito mode
@@ -11,8 +8,8 @@ import android.webkit.WebView
interface ExitCleanup {
/**
- * Clean up the instance of the browser with the provided [webView] and [context].
+ * Clean up the instance of the browser with the provided.
*/
- fun cleanUp(webView: WebView?, context: BrowserActivity)
+ fun cleanUp()
}
diff --git a/app/src/main/java/acr/browser/lightning/browser/cleanup/NormalExitCleanup.kt b/app/src/main/java/acr/browser/lightning/browser/cleanup/NormalExitCleanup.kt
index 73b7a3d72..6b0907cfd 100644
--- a/app/src/main/java/acr/browser/lightning/browser/cleanup/NormalExitCleanup.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/NormalExitCleanup.kt
@@ -1,12 +1,11 @@
package acr.browser.lightning.browser.cleanup
-import acr.browser.lightning.browser.activity.BrowserActivity
+import acr.browser.lightning.browser.di.DatabaseScheduler
import acr.browser.lightning.database.history.HistoryDatabase
-import acr.browser.lightning.di.DatabaseScheduler
import acr.browser.lightning.log.Logger
import acr.browser.lightning.preference.UserPreferences
import acr.browser.lightning.utils.WebUtils
-import android.webkit.WebView
+import android.app.Activity
import io.reactivex.Scheduler
import javax.inject.Inject
@@ -17,19 +16,20 @@ class NormalExitCleanup @Inject constructor(
private val userPreferences: UserPreferences,
private val logger: Logger,
private val historyDatabase: HistoryDatabase,
- @DatabaseScheduler private val databaseScheduler: Scheduler
+ @DatabaseScheduler private val databaseScheduler: Scheduler,
+ private val activity: Activity
) : ExitCleanup {
- override fun cleanUp(webView: WebView?, context: BrowserActivity) {
+ override fun cleanUp() {
if (userPreferences.clearCacheExit) {
- WebUtils.clearCache(webView)
+ WebUtils.clearCache(activity)
logger.log(TAG, "Cache Cleared")
}
if (userPreferences.clearHistoryExitEnabled) {
- WebUtils.clearHistory(context, historyDatabase, databaseScheduler)
+ WebUtils.clearHistory(activity, historyDatabase, databaseScheduler)
logger.log(TAG, "History Cleared")
}
if (userPreferences.clearCookiesExitEnabled) {
- WebUtils.clearCookies(context)
+ WebUtils.clearCookies()
logger.log(TAG, "Cookies Cleared")
}
if (userPreferences.clearWebStorageExitEnabled) {
diff --git a/app/src/main/java/acr/browser/lightning/browser/color/ColorAnimator.kt b/app/src/main/java/acr/browser/lightning/browser/color/ColorAnimator.kt
new file mode 100644
index 000000000..75cb9501d
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/color/ColorAnimator.kt
@@ -0,0 +1,52 @@
+package acr.browser.lightning.browser.color
+
+import acr.browser.lightning.utils.DrawableUtils
+import acr.browser.lightning.utils.Utils
+import android.graphics.Color
+import android.view.animation.Animation
+import android.view.animation.Transformation
+
+/**
+ * An animator that creates animations between two different colors.
+ */
+class ColorAnimator(private val defaultColor: Int) {
+
+ private var currentColor: Int? = null
+
+ private fun mixSearchBarColor(requestedColor: Int, defaultColor: Int): Int =
+ if (requestedColor == defaultColor) {
+ Color.WHITE
+ } else {
+ DrawableUtils.mixColor(0.25f, requestedColor, Color.WHITE)
+ }
+
+ /**
+ * Creates an animation that animates from the current color to the new [color].
+ */
+ fun animateTo(
+ color: Int,
+ onChange: (mainColor: Int, secondaryColor: Int) -> Unit
+ ): Animation {
+ val currentUiColor = currentColor ?: defaultColor
+ val finalUiColor = if (Utils.isColorGrayscale(color)) {
+ defaultColor
+ } else {
+ color
+ }
+
+ val startSearchColor = mixSearchBarColor(currentUiColor, defaultColor)
+ val finalSearchColor = mixSearchBarColor(finalUiColor, defaultColor)
+ val animation = object : Animation() {
+ override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
+ val mainColor =
+ DrawableUtils.mixColor(interpolatedTime, currentUiColor, finalUiColor)
+ val secondaryColor =
+ DrawableUtils.mixColor(interpolatedTime, startSearchColor, finalSearchColor)
+ onChange(mainColor, secondaryColor)
+ currentColor = mainColor
+ }
+ }
+ animation.duration = 300
+ return animation
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/data/CookieAdministrator.kt b/app/src/main/java/acr/browser/lightning/browser/data/CookieAdministrator.kt
new file mode 100644
index 000000000..ef98a0139
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/data/CookieAdministrator.kt
@@ -0,0 +1,13 @@
+package acr.browser.lightning.browser.data
+
+/**
+ * The administrator used to manage browser cookie preferences.
+ */
+interface CookieAdministrator {
+
+ /**
+ * Adjust the cookie setting based on the current preferences.
+ */
+ fun adjustCookieSettings()
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/data/DefaultCookieAdministrator.kt b/app/src/main/java/acr/browser/lightning/browser/data/DefaultCookieAdministrator.kt
new file mode 100644
index 000000000..d8ce42a08
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/data/DefaultCookieAdministrator.kt
@@ -0,0 +1,16 @@
+package acr.browser.lightning.browser.data
+
+import acr.browser.lightning.preference.UserPreferences
+import android.webkit.CookieManager
+import javax.inject.Inject
+
+/**
+ * The default cookie administrator that sets cookie preferences for the default browser instance.
+ */
+class DefaultCookieAdministrator @Inject constructor(
+ private val userPreferences: UserPreferences
+) : CookieAdministrator {
+ override fun adjustCookieSettings() {
+ CookieManager.getInstance().setAcceptCookie(userPreferences.cookiesEnabled)
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/data/IncognitoCookieAdministrator.kt b/app/src/main/java/acr/browser/lightning/browser/data/IncognitoCookieAdministrator.kt
new file mode 100644
index 000000000..341f5e112
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/data/IncognitoCookieAdministrator.kt
@@ -0,0 +1,23 @@
+package acr.browser.lightning.browser.data
+
+import acr.browser.lightning.Capabilities
+import acr.browser.lightning.isSupported
+import acr.browser.lightning.preference.UserPreferences
+import android.webkit.CookieManager
+import javax.inject.Inject
+
+/**
+ * The cookie administrator used to set cookie preferences for the incognito instance.
+ */
+class IncognitoCookieAdministrator @Inject constructor(
+ private val userPreferences: UserPreferences
+) : CookieAdministrator {
+ override fun adjustCookieSettings() {
+ val cookieManager = CookieManager.getInstance()
+ if (Capabilities.FULL_INCOGNITO.isSupported) {
+ cookieManager.setAcceptCookie(userPreferences.cookiesEnabled)
+ } else {
+ cookieManager.setAcceptCookie(userPreferences.incognitoCookiesEnabled)
+ }
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/di/AppBindsModule.kt b/app/src/main/java/acr/browser/lightning/browser/di/AppBindsModule.kt
similarity index 90%
rename from app/src/main/java/acr/browser/lightning/di/AppBindsModule.kt
rename to app/src/main/java/acr/browser/lightning/browser/di/AppBindsModule.kt
index 152f58cf2..bc39fa274 100644
--- a/app/src/main/java/acr/browser/lightning/di/AppBindsModule.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/di/AppBindsModule.kt
@@ -1,4 +1,4 @@
-package acr.browser.lightning.di
+package acr.browser.lightning.browser.di
import acr.browser.lightning.adblock.allowlist.AllowListModel
import acr.browser.lightning.adblock.allowlist.SessionAllowListModel
@@ -6,8 +6,6 @@ import acr.browser.lightning.adblock.source.AssetsHostsDataSource
import acr.browser.lightning.adblock.source.HostsDataSource
import acr.browser.lightning.adblock.source.HostsDataSourceProvider
import acr.browser.lightning.adblock.source.PreferencesHostsDataSourceProvider
-import acr.browser.lightning.browser.cleanup.DelegatingExitCleanup
-import acr.browser.lightning.browser.cleanup.ExitCleanup
import acr.browser.lightning.database.adblock.HostsDatabase
import acr.browser.lightning.database.adblock.HostsRepository
import acr.browser.lightning.database.allowlist.AdBlockAllowListDatabase
@@ -29,9 +27,6 @@ import dagger.Module
@Module
interface AppBindsModule {
- @Binds
- fun bindsExitCleanup(delegatingExitCleanup: DelegatingExitCleanup): ExitCleanup
-
@Binds
fun bindsBookmarkModel(bookmarkDatabase: BookmarkDatabase): BookmarkRepository
diff --git a/app/src/main/java/acr/browser/lightning/di/AppComponent.kt b/app/src/main/java/acr/browser/lightning/browser/di/AppComponent.kt
similarity index 68%
rename from app/src/main/java/acr/browser/lightning/di/AppComponent.kt
rename to app/src/main/java/acr/browser/lightning/browser/di/AppComponent.kt
index c2987ebc5..fcf46a5ef 100644
--- a/app/src/main/java/acr/browser/lightning/di/AppComponent.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/di/AppComponent.kt
@@ -1,12 +1,10 @@
-package acr.browser.lightning.di
+package acr.browser.lightning.browser.di
import acr.browser.lightning.BrowserApp
import acr.browser.lightning.adblock.BloomFilterAdBlocker
import acr.browser.lightning.adblock.NoOpAdBlocker
-import acr.browser.lightning.browser.SearchBoxModel
-import acr.browser.lightning.browser.activity.BrowserActivity
-import acr.browser.lightning.browser.activity.ThemableBrowserActivity
-import acr.browser.lightning.browser.bookmarks.BookmarksDrawerView
+import acr.browser.lightning.browser.search.SearchBoxModel
+import acr.browser.lightning.ThemableBrowserActivity
import acr.browser.lightning.device.BuildInfo
import acr.browser.lightning.dialog.LightningDialogBuilder
import acr.browser.lightning.download.LightningDownloadListener
@@ -14,17 +12,21 @@ import acr.browser.lightning.reading.activity.ReadingActivity
import acr.browser.lightning.search.SuggestionsAdapter
import acr.browser.lightning.settings.activity.SettingsActivity
import acr.browser.lightning.settings.activity.ThemableSettingsActivity
-import acr.browser.lightning.settings.fragment.*
-import acr.browser.lightning.view.LightningChromeClient
-import acr.browser.lightning.view.LightningView
-import acr.browser.lightning.view.LightningWebClient
+import acr.browser.lightning.settings.fragment.AdBlockSettingsFragment
+import acr.browser.lightning.settings.fragment.AdvancedSettingsFragment
+import acr.browser.lightning.settings.fragment.BookmarkSettingsFragment
+import acr.browser.lightning.settings.fragment.DebugSettingsFragment
+import acr.browser.lightning.settings.fragment.DisplaySettingsFragment
+import acr.browser.lightning.settings.fragment.GeneralSettingsFragment
+import acr.browser.lightning.settings.fragment.PrivacySettingsFragment
import android.app.Application
import dagger.BindsInstance
import dagger.Component
+import dagger.Module
import javax.inject.Singleton
@Singleton
-@Component(modules = [(AppModule::class), (AppBindsModule::class)])
+@Component(modules = [AppModule::class, AppBindsModule::class, Submodules::class])
interface AppComponent {
@Component.Builder
@@ -39,14 +41,10 @@ interface AppComponent {
fun build(): AppComponent
}
- fun inject(activity: BrowserActivity)
-
fun inject(fragment: BookmarkSettingsFragment)
fun inject(builder: LightningDialogBuilder)
- fun inject(lightningView: LightningView)
-
fun inject(activity: ThemableBrowserActivity)
fun inject(advancedSettingsFragment: AdvancedSettingsFragment)
@@ -55,8 +53,6 @@ interface AppComponent {
fun inject(activity: ReadingActivity)
- fun inject(webClient: LightningWebClient)
-
fun inject(activity: SettingsActivity)
fun inject(activity: ThemableSettingsActivity)
@@ -69,8 +65,6 @@ interface AppComponent {
fun inject(suggestionsAdapter: SuggestionsAdapter)
- fun inject(chromeClient: LightningChromeClient)
-
fun inject(searchBoxModel: SearchBoxModel)
fun inject(generalSettingsFragment: GeneralSettingsFragment)
@@ -79,10 +73,13 @@ interface AppComponent {
fun inject(adBlockSettingsFragment: AdBlockSettingsFragment)
- fun inject(bookmarksView: BookmarksDrawerView)
-
fun provideBloomFilterAdBlocker(): BloomFilterAdBlocker
fun provideNoOpAdBlocker(): NoOpAdBlocker
+ fun browser2ComponentBuilder(): Browser2Component.Builder
+
}
+
+@Module(subcomponents = [Browser2Component::class])
+internal class Submodules
diff --git a/app/src/main/java/acr/browser/lightning/di/AppModule.kt b/app/src/main/java/acr/browser/lightning/browser/di/AppModule.kt
similarity index 63%
rename from app/src/main/java/acr/browser/lightning/di/AppModule.kt
rename to app/src/main/java/acr/browser/lightning/browser/di/AppModule.kt
index 9cde54745..cc066707d 100644
--- a/app/src/main/java/acr/browser/lightning/di/AppModule.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/di/AppModule.kt
@@ -1,5 +1,7 @@
-package acr.browser.lightning.di
+package acr.browser.lightning.browser.di
+import acr.browser.lightning.R
+import acr.browser.lightning.browser.tab.DefaultTabTitle
import acr.browser.lightning.device.BuildInfo
import acr.browser.lightning.device.BuildType
import acr.browser.lightning.html.ListPageReader
@@ -37,7 +39,12 @@ import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import net.i2p.android.ui.I2PAndroidHelper
-import okhttp3.*
+import okhttp3.Cache
+import okhttp3.CacheControl
+import okhttp3.HttpUrl
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.Request
import java.io.File
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingDeque
@@ -58,40 +65,50 @@ class AppModule {
@Provides
@UserPrefs
- fun provideUserPreferences(application: Application): SharedPreferences = application.getSharedPreferences("settings", 0)
+ fun provideUserPreferences(application: Application): SharedPreferences =
+ application.getSharedPreferences("settings", 0)
@Provides
@DevPrefs
- fun provideDebugPreferences(application: Application): SharedPreferences = application.getSharedPreferences("developer_settings", 0)
+ fun provideDebugPreferences(application: Application): SharedPreferences =
+ application.getSharedPreferences("developer_settings", 0)
@Provides
@AdBlockPrefs
- fun provideAdBlockPreferences(application: Application): SharedPreferences = application.getSharedPreferences("ad_block_settings", 0)
+ fun provideAdBlockPreferences(application: Application): SharedPreferences =
+ application.getSharedPreferences("ad_block_settings", 0)
@Provides
fun providesAssetManager(application: Application): AssetManager = application.assets
@Provides
- fun providesClipboardManager(application: Application) = application.getSystemService()!!
+ fun providesClipboardManager(application: Application) =
+ application.getSystemService()!!
@Provides
- fun providesInputMethodManager(application: Application) = application.getSystemService()!!
+ fun providesInputMethodManager(application: Application) =
+ application.getSystemService()!!
@Provides
- fun providesDownloadManager(application: Application) = application.getSystemService()!!
+ fun providesDownloadManager(application: Application) =
+ application.getSystemService()!!
@Provides
- fun providesConnectivityManager(application: Application) = application.getSystemService()!!
+ fun providesConnectivityManager(application: Application) =
+ application.getSystemService()!!
@Provides
- fun providesNotificationManager(application: Application) = application.getSystemService()!!
+ fun providesNotificationManager(application: Application) =
+ application.getSystemService()!!
@Provides
- fun providesWindowManager(application: Application) = application.getSystemService()!!
+ fun providesWindowManager(application: Application) =
+ application.getSystemService()!!
@RequiresApi(Build.VERSION_CODES.N_MR1)
@Provides
- fun providesShortcutManager(application: Application) = application.getSystemService()!!
+ fun providesShortcutManager(application: Application) =
+ application.getSystemService()!!
@Provides
@DatabaseScheduler
@@ -106,7 +123,8 @@ class AppModule {
@Provides
@NetworkScheduler
@Singleton
- fun providesNetworkThread(): Scheduler = Schedulers.from(ThreadPoolExecutor(0, 4, 60, TimeUnit.SECONDS, LinkedBlockingDeque()))
+ fun providesNetworkThread(): Scheduler =
+ Schedulers.from(ThreadPoolExecutor(0, 4, 60, TimeUnit.SECONDS, LinkedBlockingDeque()))
@Provides
@MainScheduler
@@ -115,18 +133,20 @@ class AppModule {
@Singleton
@Provides
- fun providesSuggestionsCacheControl(): CacheControl = CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build()
+ fun providesSuggestionsCacheControl(): CacheControl =
+ CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build()
@Singleton
@Provides
- fun providesSuggestionsRequestFactory(cacheControl: CacheControl): RequestFactory = object : RequestFactory {
- override fun createSuggestionsRequest(httpUrl: HttpUrl, encoding: String): Request {
- return Request.Builder().url(httpUrl)
- .addHeader("Accept-Charset", encoding)
- .cacheControl(cacheControl)
- .build()
+ fun providesSuggestionsRequestFactory(cacheControl: CacheControl): RequestFactory =
+ object : RequestFactory {
+ override fun createSuggestionsRequest(httpUrl: HttpUrl, encoding: String): Request {
+ return Request.Builder().url(httpUrl)
+ .addHeader("Accept-Charset", encoding)
+ .cacheControl(cacheControl)
+ .build()
+ }
}
- }
private fun createInterceptorWithMaxCacheAge(maxCacheAgeSeconds: Long) = Interceptor { chain ->
chain.proceed(chain.request()).newBuilder()
@@ -137,28 +157,30 @@ class AppModule {
@Singleton
@Provides
@SuggestionsClient
- fun providesSuggestionsHttpClient(application: Application): Single = Single.fromCallable {
- val intervalDay = TimeUnit.DAYS.toSeconds(1)
- val suggestionsCache = File(application.cacheDir, "suggestion_responses")
-
- return@fromCallable OkHttpClient.Builder()
- .cache(Cache(suggestionsCache, FileUtils.megabytesToBytes(1)))
- .addNetworkInterceptor(createInterceptorWithMaxCacheAge(intervalDay))
- .build()
- }.cache()
+ fun providesSuggestionsHttpClient(application: Application): Single =
+ Single.fromCallable {
+ val intervalDay = TimeUnit.DAYS.toSeconds(1)
+ val suggestionsCache = File(application.cacheDir, "suggestion_responses")
+
+ return@fromCallable OkHttpClient.Builder()
+ .cache(Cache(suggestionsCache, FileUtils.megabytesToBytes(1)))
+ .addNetworkInterceptor(createInterceptorWithMaxCacheAge(intervalDay))
+ .build()
+ }.cache()
@Singleton
@Provides
@HostsClient
- fun providesHostsHttpClient(application: Application): Single = Single.fromCallable {
- val intervalYear = TimeUnit.DAYS.toSeconds(365)
- val suggestionsCache = File(application.cacheDir, "hosts_cache")
-
- return@fromCallable OkHttpClient.Builder()
- .cache(Cache(suggestionsCache, FileUtils.megabytesToBytes(5)))
- .addNetworkInterceptor(createInterceptorWithMaxCacheAge(intervalYear))
- .build()
- }.cache()
+ fun providesHostsHttpClient(application: Application): Single =
+ Single.fromCallable {
+ val intervalYear = TimeUnit.DAYS.toSeconds(365)
+ val suggestionsCache = File(application.cacheDir, "hosts_cache")
+
+ return@fromCallable OkHttpClient.Builder()
+ .cache(Cache(suggestionsCache, FileUtils.megabytesToBytes(5)))
+ .addNetworkInterceptor(createInterceptorWithMaxCacheAge(intervalYear))
+ .build()
+ }.cache()
@Provides
@Singleton
@@ -170,7 +192,8 @@ class AppModule {
@Provides
@Singleton
- fun provideI2PAndroidHelper(application: Application): I2PAndroidHelper = I2PAndroidHelper(application)
+ fun provideI2PAndroidHelper(application: Application): I2PAndroidHelper =
+ I2PAndroidHelper(application)
@Provides
fun providesListPageReader(): ListPageReader = MezzanineGenerator.ListPageReader()
@@ -190,6 +213,11 @@ class AppModule {
@Provides
fun providesInvertPage(): InvertPage = MezzanineGenerator.InvertPage()
+ @DefaultTabTitle
+ @Provides
+ fun providesDefaultTabTitle(application: Application): String =
+ application.getString(R.string.untitled)
+
}
@Qualifier
diff --git a/app/src/main/java/acr/browser/lightning/browser/di/Browser2BindsModule.kt b/app/src/main/java/acr/browser/lightning/browser/di/Browser2BindsModule.kt
new file mode 100644
index 000000000..d5df628bb
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/di/Browser2BindsModule.kt
@@ -0,0 +1,40 @@
+package acr.browser.lightning.browser.di
+
+import acr.browser.lightning.browser.BrowserContract
+import acr.browser.lightning.browser.BrowserNavigator
+import acr.browser.lightning.browser.cleanup.DelegatingExitCleanup
+import acr.browser.lightning.browser.cleanup.ExitCleanup
+import acr.browser.lightning.browser.image.FaviconImageLoader
+import acr.browser.lightning.browser.image.ImageLoader
+import acr.browser.lightning.browser.proxy.Proxy
+import acr.browser.lightning.browser.proxy.ProxyAdapter
+import acr.browser.lightning.browser.tab.TabsRepository
+import acr.browser.lightning.browser.theme.DefaultThemeProvider
+import acr.browser.lightning.browser.theme.ThemeProvider
+import dagger.Binds
+import dagger.Module
+
+/**
+ * Binds implementations to interfaces for the browser scope.
+ */
+@Module
+interface Browser2BindsModule {
+
+ @Binds
+ fun bindsBrowserModel(tabsRepository: TabsRepository): BrowserContract.Model
+
+ @Binds
+ fun bindsFaviconImageLoader(faviconImageLoader: FaviconImageLoader): ImageLoader
+
+ @Binds
+ fun bindsBrowserNavigator(browserNavigator: BrowserNavigator): BrowserContract.Navigator
+
+ @Binds
+ fun bindsProxy(proxyAdapter: ProxyAdapter): Proxy
+
+ @Binds
+ fun bindsExitCleanup(delegatingExitCleanup: DelegatingExitCleanup): ExitCleanup
+
+ @Binds
+ fun bindsThemeProvider(legacyThemeProvider: DefaultThemeProvider): ThemeProvider
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/di/Browser2Component.kt b/app/src/main/java/acr/browser/lightning/browser/di/Browser2Component.kt
new file mode 100644
index 000000000..0635f055f
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/di/Browser2Component.kt
@@ -0,0 +1,56 @@
+package acr.browser.lightning.browser.di
+
+import acr.browser.lightning.browser.BrowserActivity
+import android.app.Activity
+import android.content.Intent
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import dagger.BindsInstance
+import dagger.Subcomponent
+import javax.inject.Qualifier
+
+/**
+ * The component for the browser scope.
+ */
+@Browser2Scope
+@Subcomponent(modules = [Browser2Module::class, Browser2BindsModule::class])
+interface Browser2Component {
+
+ @Subcomponent.Builder
+ interface Builder {
+
+ @BindsInstance
+ fun activity(activity: Activity): Builder
+
+ @BindsInstance
+ fun browserFrame(frameLayout: FrameLayout): Builder
+
+ @BindsInstance
+ fun toolbarRoot(linearLayout: LinearLayout): Builder
+
+ @BindsInstance
+ fun toolbar(toolbar: View): Builder
+
+ @BindsInstance
+ fun initialIntent(@InitialIntent intent: Intent): Builder
+
+ @BindsInstance
+ fun incognitoMode(@IncognitoMode incognitoMode: Boolean): Builder
+
+ fun build(): Browser2Component
+
+ }
+
+ fun inject(browserActivity: BrowserActivity)
+
+}
+
+@Qualifier
+annotation class InitialIntent
+
+@Qualifier
+annotation class InitialUrl
+
+@Qualifier
+annotation class IncognitoMode
diff --git a/app/src/main/java/acr/browser/lightning/browser/di/Browser2Module.kt b/app/src/main/java/acr/browser/lightning/browser/di/Browser2Module.kt
new file mode 100644
index 000000000..fa2b13a86
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/di/Browser2Module.kt
@@ -0,0 +1,136 @@
+package acr.browser.lightning.browser.di
+
+import acr.browser.lightning.R
+import acr.browser.lightning.browser.BrowserContract
+import acr.browser.lightning.browser.data.CookieAdministrator
+import acr.browser.lightning.browser.data.DefaultCookieAdministrator
+import acr.browser.lightning.browser.history.DefaultHistoryRecord
+import acr.browser.lightning.browser.history.HistoryRecord
+import acr.browser.lightning.browser.history.NoOpHistoryRecord
+import acr.browser.lightning.browser.image.IconFreeze
+import acr.browser.lightning.browser.notification.DefaultTabCountNotifier
+import acr.browser.lightning.browser.notification.IncognitoTabCountNotifier
+import acr.browser.lightning.browser.notification.TabCountNotifier
+import acr.browser.lightning.browser.search.IntentExtractor
+import acr.browser.lightning.browser.tab.DefaultUserAgent
+import acr.browser.lightning.browser.tab.bundle.BundleStore
+import acr.browser.lightning.browser.tab.bundle.DefaultBundleStore
+import acr.browser.lightning.browser.tab.bundle.IncognitoBundleStore
+import acr.browser.lightning.browser.ui.BookmarkConfiguration
+import acr.browser.lightning.browser.ui.TabConfiguration
+import acr.browser.lightning.browser.ui.UiConfiguration
+import acr.browser.lightning.adblock.AdBlocker
+import acr.browser.lightning.adblock.BloomFilterAdBlocker
+import acr.browser.lightning.adblock.NoOpAdBlocker
+import acr.browser.lightning.extensions.drawable
+import acr.browser.lightning.preference.UserPreferences
+import acr.browser.lightning.utils.IntentUtils
+import android.app.Activity
+import android.app.Application
+import android.content.Intent
+import android.graphics.Bitmap
+import android.webkit.WebSettings
+import androidx.core.graphics.drawable.toBitmap
+import dagger.Module
+import dagger.Provides
+import javax.inject.Provider
+
+/**
+ * Constructs dependencies for the browser scope.
+ */
+@Module
+class Browser2Module {
+
+ @Provides
+ fun providesAdBlocker(
+ userPreferences: UserPreferences,
+ bloomFilterAdBlocker: Provider,
+ noOpAdBlocker: NoOpAdBlocker
+ ): AdBlocker = if (userPreferences.adBlockEnabled) {
+ bloomFilterAdBlocker.get()
+ } else {
+ noOpAdBlocker
+ }
+
+ // TODO: dont force cast
+ @Provides
+ @InitialUrl
+ fun providesInitialUrl(
+ @InitialIntent initialIntent: Intent,
+ intentExtractor: IntentExtractor
+ ): String? =
+ (intentExtractor.extractUrlFromIntent(initialIntent) as? BrowserContract.Action.LoadUrl)?.url
+
+ // TODO: auto inject intent utils
+ @Provides
+ fun providesIntentUtils(activity: Activity): IntentUtils = IntentUtils(activity)
+
+ @Provides
+ fun providesUiConfiguration(
+ userPreferences: UserPreferences
+ ): UiConfiguration = UiConfiguration(
+ tabConfiguration = if (userPreferences.showTabsInDrawer) {
+ TabConfiguration.DRAWER
+ } else {
+ TabConfiguration.DESKTOP
+ },
+ bookmarkConfiguration = if (userPreferences.bookmarksAndTabsSwapped) {
+ BookmarkConfiguration.LEFT
+ } else {
+ BookmarkConfiguration.RIGHT
+ }
+ )
+
+ @DefaultUserAgent
+ @Provides
+ fun providesDefaultUserAgent(application: Application): String =
+ WebSettings.getDefaultUserAgent(application)
+
+
+ @Provides
+ fun providesHistoryRecord(
+ @IncognitoMode incognitoMode: Boolean,
+ defaultHistoryRecord: DefaultHistoryRecord
+ ): HistoryRecord = if (incognitoMode) {
+ NoOpHistoryRecord
+ } else {
+ defaultHistoryRecord
+ }
+
+ @Provides
+ fun providesCookieAdministrator(
+ @IncognitoMode incognitoMode: Boolean,
+ defaultCookieAdministrator: DefaultCookieAdministrator,
+ incognitoCookieAdministrator: DefaultCookieAdministrator
+ ): CookieAdministrator = if (incognitoMode) {
+ incognitoCookieAdministrator
+ } else {
+ defaultCookieAdministrator
+ }
+
+ @Provides
+ fun providesTabCountNotifier(
+ @IncognitoMode incognitoMode: Boolean,
+ incognitoTabCountNotifier: IncognitoTabCountNotifier
+ ): TabCountNotifier = if (incognitoMode) {
+ incognitoTabCountNotifier
+ } else {
+ DefaultTabCountNotifier
+ }
+
+ @Provides
+ fun providesBundleStore(
+ @IncognitoMode incognitoMode: Boolean,
+ defaultBundleStore: DefaultBundleStore
+ ): BundleStore = if (incognitoMode) {
+ IncognitoBundleStore
+ } else {
+ defaultBundleStore
+ }
+
+ @IconFreeze
+ @Provides
+ fun providesFrozenIcon(activity: Activity): Bitmap =
+ activity.drawable(R.drawable.ic_frozen).toBitmap()
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/di/Browser2Scope.kt b/app/src/main/java/acr/browser/lightning/browser/di/Browser2Scope.kt
new file mode 100644
index 000000000..7dcf5e220
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/di/Browser2Scope.kt
@@ -0,0 +1,10 @@
+package acr.browser.lightning.browser.di
+
+import javax.inject.Scope
+
+/**
+ * The dependency scope for the browser screen.
+ */
+@Scope
+@Retention
+annotation class Browser2Scope
diff --git a/app/src/main/java/acr/browser/lightning/di/DiExtensions.kt b/app/src/main/java/acr/browser/lightning/browser/di/DiExtensions.kt
similarity index 95%
rename from app/src/main/java/acr/browser/lightning/di/DiExtensions.kt
rename to app/src/main/java/acr/browser/lightning/browser/di/DiExtensions.kt
index 4532a3129..1fff1296a 100644
--- a/app/src/main/java/acr/browser/lightning/di/DiExtensions.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/di/DiExtensions.kt
@@ -1,6 +1,6 @@
@file:JvmName("Injector")
-package acr.browser.lightning.di
+package acr.browser.lightning.browser.di
import acr.browser.lightning.BrowserApp
import android.content.Context
diff --git a/app/src/main/java/acr/browser/lightning/browser/download/DownloadPermissionsHelper.kt b/app/src/main/java/acr/browser/lightning/browser/download/DownloadPermissionsHelper.kt
new file mode 100644
index 000000000..4c5a1ceab
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/download/DownloadPermissionsHelper.kt
@@ -0,0 +1,113 @@
+package acr.browser.lightning.browser.download
+
+import acr.browser.lightning.R
+import acr.browser.lightning.database.downloads.DownloadEntry
+import acr.browser.lightning.database.downloads.DownloadsRepository
+import acr.browser.lightning.browser.di.DatabaseScheduler
+import acr.browser.lightning.dialog.BrowserDialog.setDialogSize
+import acr.browser.lightning.download.DownloadHandler
+import acr.browser.lightning.log.Logger
+import acr.browser.lightning.preference.UserPreferences
+import android.Manifest
+import android.app.Activity
+import android.app.Dialog
+import android.content.DialogInterface
+import android.text.format.Formatter
+import android.webkit.URLUtil
+import androidx.appcompat.app.AlertDialog
+import com.anthonycr.grant.PermissionsManager
+import com.anthonycr.grant.PermissionsResultAction
+import io.reactivex.Scheduler
+import io.reactivex.rxkotlin.subscribeBy
+import javax.inject.Inject
+
+/**
+ * Wraps [DownloadHandler] for a better download API.
+ */
+class DownloadPermissionsHelper @Inject constructor(
+ private val downloadHandler: DownloadHandler,
+ private val userPreferences: UserPreferences,
+ private val logger: Logger,
+ private val downloadsRepository: DownloadsRepository,
+ @DatabaseScheduler private val databaseScheduler: Scheduler
+) {
+
+ /**
+ * Download a file with the provided [url].
+ */
+ fun download(
+ activity: Activity,
+ url: String,
+ userAgent: String?,
+ contentDisposition: String?,
+ mimeType: String?,
+ contentLength: Long
+ ) {
+ PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(activity, arrayOf(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ),
+ object : PermissionsResultAction() {
+ override fun onGranted() {
+ val fileName = URLUtil.guessFileName(url, contentDisposition, mimeType)
+ val downloadSize: String = if (contentLength > 0) {
+ Formatter.formatFileSize(activity, contentLength)
+ } else {
+ activity.getString(R.string.unknown_size)
+ }
+ val dialogClickListener = DialogInterface.OnClickListener { _, which: Int ->
+ when (which) {
+ DialogInterface.BUTTON_POSITIVE -> {
+ downloadHandler.onDownloadStart(
+ activity,
+ userPreferences,
+ url,
+ userAgent,
+ contentDisposition,
+ mimeType,
+ downloadSize
+ )
+ downloadsRepository.addDownloadIfNotExists(
+ DownloadEntry(
+ url = url,
+ title = fileName,
+ contentSize = downloadSize
+ )
+ ).subscribeOn(databaseScheduler)
+ .subscribeBy {
+ if (!it) {
+ logger.log(TAG, "error saving download to database")
+ }
+ }
+ }
+ DialogInterface.BUTTON_NEGATIVE -> {
+ }
+ }
+ }
+ val builder = AlertDialog.Builder(activity) // dialog
+ val message: String = activity.getString(R.string.dialog_download, downloadSize)
+ val dialog: Dialog = builder.setTitle(fileName)
+ .setMessage(message)
+ .setPositiveButton(
+ activity.resources.getString(R.string.action_download),
+ dialogClickListener
+ )
+ .setNegativeButton(
+ activity.resources.getString(R.string.action_cancel),
+ dialogClickListener
+ ).show()
+ setDialogSize(activity, dialog)
+ logger.log(TAG, "Downloading: $fileName")
+ }
+
+ override fun onDenied(permission: String) {
+ //TODO show message
+ logger.log(TAG, "Download permission denied")
+ }
+ })
+ }
+
+ companion object {
+ private const val TAG = "DownloadPermissionsHelper"
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/download/PendingDownload.kt b/app/src/main/java/acr/browser/lightning/browser/download/PendingDownload.kt
new file mode 100644
index 000000000..5b618d373
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/download/PendingDownload.kt
@@ -0,0 +1,18 @@
+package acr.browser.lightning.browser.download
+
+/**
+ * Represents a file that will be downloaded.
+ *
+ * @param url The URL of the file.
+ * @param userAgent The user agent we will use when making the download request.
+ * @param contentDisposition The description of content we are downloading.
+ * @param mimeType The type of content we are downloading.
+ * @param contentLength The size of the file we are downloading.
+ */
+data class PendingDownload(
+ val url: String,
+ val userAgent: String?,
+ val contentDisposition: String?,
+ val mimeType: String?,
+ val contentLength: Long
+)
diff --git a/app/src/main/java/acr/browser/lightning/browser/history/DefaultHistoryRecord.kt b/app/src/main/java/acr/browser/lightning/browser/history/DefaultHistoryRecord.kt
new file mode 100644
index 000000000..999cc9788
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/history/DefaultHistoryRecord.kt
@@ -0,0 +1,20 @@
+package acr.browser.lightning.browser.history
+
+import acr.browser.lightning.database.history.HistoryRepository
+import acr.browser.lightning.browser.di.DatabaseScheduler
+import io.reactivex.Scheduler
+import javax.inject.Inject
+
+/**
+ * The default history record that records the history in a permanent data store.
+ */
+class DefaultHistoryRecord @Inject constructor(
+ private val historyRepository: HistoryRepository,
+ @DatabaseScheduler private val databaseScheduler: Scheduler
+) : HistoryRecord {
+ override fun recordVisit(title: String, url: String) {
+ historyRepository.visitHistoryEntry(url, title)
+ .subscribeOn(databaseScheduler)
+ .subscribe()
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/history/HistoryRecord.kt b/app/src/main/java/acr/browser/lightning/browser/history/HistoryRecord.kt
new file mode 100644
index 000000000..953c627c5
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/history/HistoryRecord.kt
@@ -0,0 +1,13 @@
+package acr.browser.lightning.browser.history
+
+/**
+ * Records browser history.
+ */
+interface HistoryRecord {
+
+ /**
+ * Record a visit to the [url] with the provided [title].
+ */
+ fun recordVisit(title: String, url: String)
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/history/NoOpHistoryRecord.kt b/app/src/main/java/acr/browser/lightning/browser/history/NoOpHistoryRecord.kt
new file mode 100644
index 000000000..eb904a46c
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/history/NoOpHistoryRecord.kt
@@ -0,0 +1,8 @@
+package acr.browser.lightning.browser.history
+
+/**
+ * A non functional history record that ignores all attempts to record a visit.
+ */
+object NoOpHistoryRecord : HistoryRecord {
+ override fun recordVisit(title: String, url: String) = Unit
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/image/FaviconImageLoader.kt b/app/src/main/java/acr/browser/lightning/browser/image/FaviconImageLoader.kt
new file mode 100644
index 000000000..5c22f49f4
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/image/FaviconImageLoader.kt
@@ -0,0 +1,73 @@
+package acr.browser.lightning.browser.image
+
+import acr.browser.lightning.R
+import acr.browser.lightning.database.Bookmark
+import acr.browser.lightning.browser.di.MainScheduler
+import acr.browser.lightning.browser.di.NetworkScheduler
+import acr.browser.lightning.extensions.drawable
+import acr.browser.lightning.favicon.FaviconModel
+import android.app.Application
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.util.LruCache
+import android.widget.ImageView
+import io.reactivex.Scheduler
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.rxkotlin.plusAssign
+import io.reactivex.rxkotlin.subscribeBy
+import javax.inject.Inject
+
+/**
+ * An image loader implementation that caches icons in memory after reading them from the disk
+ * cache.
+ */
+class FaviconImageLoader @Inject constructor(
+ private val faviconModel: FaviconModel,
+ application: Application,
+ @NetworkScheduler private val networkScheduler: Scheduler,
+ @MainScheduler private val mainScheduler: Scheduler
+) : ImageLoader {
+
+ private val lruCache: LruCache = LruCache(1 * 1000 * 1000)
+ private val folderIcon = application.drawable(R.drawable.ic_folder)
+ private val webPageIcon = application.drawable(R.drawable.ic_webpage)
+ private val compositeDisposable = CompositeDisposable()
+
+ override fun loadImage(imageView: ImageView, bookmark: Bookmark) {
+ imageView.tag = bookmark.url
+ lruCache[bookmark.url]?.let {
+ if (it is Bitmap) {
+ imageView.setImageBitmap(it)
+ } else if (it is Drawable) {
+ imageView.setImageDrawable(it)
+ }
+ } ?: run {
+ when (bookmark) {
+ is Bookmark.Folder -> {
+ lruCache.put(bookmark.url, folderIcon)
+ imageView.setImageDrawable(folderIcon)
+ }
+ is Bookmark.Entry -> {
+ lruCache.put(bookmark.url, webPageIcon)
+ imageView.setImageDrawable(webPageIcon)
+ compositeDisposable += faviconModel
+ .faviconForUrl(bookmark.url, bookmark.title)
+ .subscribeOn(networkScheduler)
+ .observeOn(mainScheduler)
+ .subscribeBy(
+ onSuccess = { bitmap ->
+ lruCache.put(bookmark.url, bitmap)
+ if (imageView.tag == bookmark.url) {
+ imageView.setImageBitmap(bitmap)
+ }
+ }
+ )
+ }
+ }
+ }
+
+ fun cleanup() {
+ compositeDisposable.clear()
+ }
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/image/IconFreeze.kt b/app/src/main/java/acr/browser/lightning/browser/image/IconFreeze.kt
new file mode 100644
index 000000000..d74724f0e
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/image/IconFreeze.kt
@@ -0,0 +1,9 @@
+package acr.browser.lightning.browser.image
+
+import javax.inject.Qualifier
+
+/**
+ * The frozen icon qualifier
+ */
+@Qualifier
+annotation class IconFreeze
diff --git a/app/src/main/java/acr/browser/lightning/browser/image/ImageLoader.kt b/app/src/main/java/acr/browser/lightning/browser/image/ImageLoader.kt
new file mode 100644
index 000000000..77b4f11bd
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/image/ImageLoader.kt
@@ -0,0 +1,16 @@
+package acr.browser.lightning.browser.image
+
+import acr.browser.lightning.database.Bookmark
+import android.widget.ImageView
+
+/**
+ * Loads images for bookmark entries.
+ */
+interface ImageLoader {
+
+ /**
+ * Load a the favicon into the [imageView] for the provided [bookmark].
+ */
+ fun loadImage(imageView: ImageView, bookmark: Bookmark)
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/keys/KeyCombo.kt b/app/src/main/java/acr/browser/lightning/browser/keys/KeyCombo.kt
new file mode 100644
index 000000000..17e033042
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/keys/KeyCombo.kt
@@ -0,0 +1,25 @@
+package acr.browser.lightning.browser.keys
+
+/**
+ * Supported key shortcut combinations.
+ */
+enum class KeyCombo {
+ CTRL_F,
+ CTRL_T,
+ CTRL_W,
+ CTRL_Q,
+ CTRL_R,
+ CTRL_TAB,
+ CTRL_SHIFT_TAB,
+ SEARCH,
+ ALT_0,
+ ALT_1,
+ ALT_2,
+ ALT_3,
+ ALT_4,
+ ALT_5,
+ ALT_6,
+ ALT_7,
+ ALT_8,
+ ALT_9,
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/keys/KeyEventAdapter.kt b/app/src/main/java/acr/browser/lightning/browser/keys/KeyEventAdapter.kt
new file mode 100644
index 000000000..679afe527
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/keys/KeyEventAdapter.kt
@@ -0,0 +1,74 @@
+package acr.browser.lightning.browser.keys
+
+import android.view.KeyEvent
+import javax.inject.Inject
+
+/**
+ * Adapts [KeyEvents][KeyEvent] to [KeyCombos][KeyCombo].
+ */
+class KeyEventAdapter @Inject constructor() {
+
+ /**
+ * Adapt the [event] or return null if the key combo is unsupported.
+ */
+ fun adaptKeyEvent(event: KeyEvent): KeyCombo? {
+ when {
+ event.isCtrlPressed -> when (event.keyCode) {
+ KeyEvent.KEYCODE_F -> {
+ // Search in page
+ return KeyCombo.CTRL_F
+ }
+ KeyEvent.KEYCODE_T -> {
+ // New tab
+ return KeyCombo.CTRL_T
+ }
+ KeyEvent.KEYCODE_W -> {
+ // Close current tab
+ return KeyCombo.CTRL_W
+ }
+ KeyEvent.KEYCODE_Q -> {
+ // Close browser
+ return KeyCombo.CTRL_Q
+ }
+ KeyEvent.KEYCODE_R -> {
+ // Refresh
+ return KeyCombo.CTRL_R
+ }
+ KeyEvent.KEYCODE_TAB -> {
+ return if (event.isShiftPressed) {
+ // Go back one tab
+ KeyCombo.CTRL_SHIFT_TAB
+ } else {
+ // Go forward one tab
+ KeyCombo.CTRL_TAB
+ }
+ }
+ }
+ event.keyCode == KeyEvent.KEYCODE_SEARCH -> {
+ // Highlight search field
+ return KeyCombo.SEARCH
+ }
+ event.isAltPressed -> {
+ // Alt + tab number
+ if (event.keyCode in KeyEvent.KEYCODE_0..KeyEvent.KEYCODE_9) {
+ // Choose tab by number
+ return when (event.keyCode) {
+ KeyEvent.KEYCODE_0 -> KeyCombo.ALT_0
+ KeyEvent.KEYCODE_1 -> KeyCombo.ALT_1
+ KeyEvent.KEYCODE_2 -> KeyCombo.ALT_2
+ KeyEvent.KEYCODE_3 -> KeyCombo.ALT_3
+ KeyEvent.KEYCODE_4 -> KeyCombo.ALT_4
+ KeyEvent.KEYCODE_5 -> KeyCombo.ALT_5
+ KeyEvent.KEYCODE_6 -> KeyCombo.ALT_6
+ KeyEvent.KEYCODE_7 -> KeyCombo.ALT_8
+ KeyEvent.KEYCODE_8 -> KeyCombo.ALT_8
+ KeyEvent.KEYCODE_9 -> KeyCombo.ALT_9
+ else -> null
+ }
+ }
+ }
+ }
+ return null
+ }
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/menu/MenuItemAdapter.kt b/app/src/main/java/acr/browser/lightning/browser/menu/MenuItemAdapter.kt
new file mode 100644
index 000000000..c4fbf2cab
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/menu/MenuItemAdapter.kt
@@ -0,0 +1,36 @@
+package acr.browser.lightning.browser.menu
+
+import acr.browser.lightning.R
+import android.view.MenuItem
+import javax.inject.Inject
+
+/**
+ * Adapts a click on a menu item to a [MenuSelection].
+ */
+class MenuItemAdapter @Inject constructor() {
+
+ /**
+ * Adapt the [menuItem] or return null if the item is unsupported.
+ */
+ fun adaptMenuItem(menuItem: MenuItem): MenuSelection? {
+ return when (menuItem.itemId) {
+ android.R.id.home -> TODO()
+ R.id.action_back -> MenuSelection.BACK
+ R.id.action_forward -> MenuSelection.FORWARD
+ R.id.action_add_to_homescreen -> MenuSelection.ADD_TO_HOME
+ R.id.action_new_tab -> MenuSelection.NEW_TAB
+ R.id.action_incognito -> MenuSelection.NEW_INCOGNITO_TAB
+ R.id.action_share -> MenuSelection.SHARE
+ R.id.action_bookmarks -> MenuSelection.BOOKMARKS
+ R.id.action_copy -> MenuSelection.COPY_LINK
+ R.id.action_settings -> MenuSelection.SETTINGS
+ R.id.action_history -> MenuSelection.HISTORY
+ R.id.action_downloads -> MenuSelection.DOWNLOADS
+ R.id.action_add_bookmark -> MenuSelection.ADD_BOOKMARK
+ R.id.action_find -> MenuSelection.FIND
+ R.id.action_reading_mode -> MenuSelection.READER
+ else -> null
+ }
+ }
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/menu/MenuSelection.kt b/app/src/main/java/acr/browser/lightning/browser/menu/MenuSelection.kt
new file mode 100644
index 000000000..bf673c509
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/menu/MenuSelection.kt
@@ -0,0 +1,21 @@
+package acr.browser.lightning.browser.menu
+
+/**
+ * Supported browser menu options.
+ */
+enum class MenuSelection {
+ NEW_TAB,
+ NEW_INCOGNITO_TAB,
+ SHARE,
+ HISTORY,
+ DOWNLOADS,
+ FIND,
+ COPY_LINK,
+ ADD_TO_HOME,
+ BOOKMARKS,
+ ADD_BOOKMARK,
+ READER,
+ SETTINGS,
+ BACK,
+ FORWARD
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/notification/DefaultTabCountNotifier.kt b/app/src/main/java/acr/browser/lightning/browser/notification/DefaultTabCountNotifier.kt
new file mode 100644
index 000000000..0634ba1eb
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/notification/DefaultTabCountNotifier.kt
@@ -0,0 +1,8 @@
+package acr.browser.lightning.browser.notification
+
+/**
+ * Do nothing when notified about the new tab count.
+ */
+object DefaultTabCountNotifier : TabCountNotifier {
+ override fun notifyTabCountChange(total: Int) = Unit
+}
diff --git a/app/src/main/java/acr/browser/lightning/notifications/IncognitoNotification.kt b/app/src/main/java/acr/browser/lightning/browser/notification/IncognitoNotification.kt
similarity index 59%
rename from app/src/main/java/acr/browser/lightning/notifications/IncognitoNotification.kt
rename to app/src/main/java/acr/browser/lightning/browser/notification/IncognitoNotification.kt
index 8655a8be1..5a14a3573 100644
--- a/app/src/main/java/acr/browser/lightning/notifications/IncognitoNotification.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/notification/IncognitoNotification.kt
@@ -1,23 +1,24 @@
-package acr.browser.lightning.notifications
+package acr.browser.lightning.browser.notification
-import acr.browser.lightning.IncognitoActivity
import acr.browser.lightning.R
+import acr.browser.lightning.IncognitoBrowserActivity
import acr.browser.lightning.utils.ThemeUtils
import android.annotation.TargetApi
+import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
-import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
+import javax.inject.Inject
/**
* A notification helper that displays the current number of tabs open in a notification as a
* warning. When the notification is pressed, the incognito browser will open.
*/
-class IncognitoNotification(
- private val context: Context,
+class IncognitoNotification @Inject constructor(
+ private val activity: Activity,
private val notificationManager: NotificationManager
) {
@@ -32,8 +33,9 @@ class IncognitoNotification(
@TargetApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
- val channelName = context.getString(R.string.notification_incognito_running_description)
- val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
+ val channelName = activity.getString(R.string.notification_incognito_running_description)
+ val channel =
+ NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
channel.enableVibration(false)
notificationManager.createNotificationChannel(channel)
}
@@ -46,15 +48,21 @@ class IncognitoNotification(
*/
fun show(number: Int) {
require(number > 0)
- val incognitoIntent = IncognitoActivity.createIntent(context)
+ val incognitoIntent = IncognitoBrowserActivity.intent(activity)
- val incognitoNotification = NotificationCompat.Builder(context, channelId)
+ val incognitoNotification = NotificationCompat.Builder(activity, channelId)
.setSmallIcon(R.drawable.ic_notification_incognito)
- .setContentTitle(context.resources.getQuantityString(R.plurals.notification_incognito_running_title, number, number))
- .setContentIntent(PendingIntent.getActivity(context, 0, incognitoIntent, 0))
- .setContentText(context.getString(R.string.notification_incognito_running_message))
+ .setContentTitle(
+ activity.resources.getQuantityString(
+ R.plurals.notification_incognito_running_title,
+ number,
+ number
+ )
+ )
+ .setContentIntent(PendingIntent.getActivity(activity, 0, incognitoIntent, 0))
+ .setContentText(activity.getString(R.string.notification_incognito_running_message))
.setAutoCancel(false)
- .setColor(ThemeUtils.getAccentColor(context))
+ .setColor(ThemeUtils.getAccentColor(activity))
.setOngoing(true)
.build()
diff --git a/app/src/main/java/acr/browser/lightning/browser/notification/IncognitoTabCountNotifier.kt b/app/src/main/java/acr/browser/lightning/browser/notification/IncognitoTabCountNotifier.kt
new file mode 100644
index 000000000..7bf30875a
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/notification/IncognitoTabCountNotifier.kt
@@ -0,0 +1,18 @@
+package acr.browser.lightning.browser.notification
+
+import javax.inject.Inject
+
+/**
+ * Shows a notification about the number of incognito tabs currently open.
+ */
+class IncognitoTabCountNotifier @Inject constructor(
+ private val incognitoNotification: IncognitoNotification
+) : TabCountNotifier {
+ override fun notifyTabCountChange(total: Int) {
+ if (total > 0) {
+ incognitoNotification.show(total)
+ } else {
+ incognitoNotification.hide()
+ }
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/notification/TabCountNotifier.kt b/app/src/main/java/acr/browser/lightning/browser/notification/TabCountNotifier.kt
new file mode 100644
index 000000000..988b8aa20
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/notification/TabCountNotifier.kt
@@ -0,0 +1,13 @@
+package acr.browser.lightning.browser.notification
+
+/**
+ * Notify the browser outside of the regular view that the tab count has changed.
+ */
+interface TabCountNotifier {
+
+ /**
+ * The open tab count has changed to the new [total].
+ */
+ fun notifyTabCountChange(total: Int)
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/proxy/Proxy.kt b/app/src/main/java/acr/browser/lightning/browser/proxy/Proxy.kt
new file mode 100644
index 000000000..c7ff9a783
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/proxy/Proxy.kt
@@ -0,0 +1,13 @@
+package acr.browser.lightning.browser.proxy
+
+/**
+ * A proxy for the proxy that determines if the proxy is ready (proxy-ception).
+ */
+interface Proxy {
+
+ /**
+ * True if the proxy is ready for use or if no proxy is being used, false otherwise.
+ */
+ fun isProxyReady(): Boolean
+
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/proxy/ProxyAdapter.kt b/app/src/main/java/acr/browser/lightning/browser/proxy/ProxyAdapter.kt
new file mode 100644
index 000000000..17d11a6a1
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/proxy/ProxyAdapter.kt
@@ -0,0 +1,52 @@
+package acr.browser.lightning.browser.proxy
+
+import acr.browser.lightning.browser.BrowserActivity
+import acr.browser.lightning.utils.ProxyUtils
+import android.app.Activity
+import android.app.Application
+import android.os.Bundle
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * An adapter between [ProxyUtils] and [Proxy].
+ */
+@Singleton
+class ProxyAdapter @Inject constructor(
+ private val proxyUtils: ProxyUtils
+) : Proxy, Application.ActivityLifecycleCallbacks {
+
+ private var currentActivity: Activity? = null
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ if (activity !is BrowserActivity) return
+ currentActivity = activity
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+ if (activity !is BrowserActivity) return
+ proxyUtils.onStart(activity)
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ if (activity !is BrowserActivity) return
+ proxyUtils.checkForProxy(activity)
+ proxyUtils.updateProxySettings(activity)
+ }
+
+ override fun onActivityPaused(activity: Activity) = Unit
+
+ override fun onActivityStopped(activity: Activity) {
+ if (activity !is BrowserActivity) return
+ proxyUtils.onStop()
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
+
+ override fun onActivityDestroyed(activity: Activity) {
+ if (activity !is BrowserActivity) return
+ currentActivity = null
+ }
+
+ override fun isProxyReady(): Boolean = currentActivity?.let(proxyUtils::isProxyReady) ?: false
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/ProxyChoice.kt b/app/src/main/java/acr/browser/lightning/browser/proxy/ProxyChoice.kt
similarity index 82%
rename from app/src/main/java/acr/browser/lightning/browser/ProxyChoice.kt
rename to app/src/main/java/acr/browser/lightning/browser/proxy/ProxyChoice.kt
index ed89b18bf..e798c20c4 100644
--- a/app/src/main/java/acr/browser/lightning/browser/ProxyChoice.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/proxy/ProxyChoice.kt
@@ -1,4 +1,4 @@
-package acr.browser.lightning.browser
+package acr.browser.lightning.browser.proxy
import acr.browser.lightning.preference.IntEnum
diff --git a/app/src/main/java/acr/browser/lightning/browser/search/IntentExtractor.kt b/app/src/main/java/acr/browser/lightning/browser/search/IntentExtractor.kt
new file mode 100644
index 000000000..e2aab796a
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/search/IntentExtractor.kt
@@ -0,0 +1,42 @@
+package acr.browser.lightning.browser.search
+
+import acr.browser.lightning.browser.BrowserContract
+import acr.browser.lightning.search.SearchEngineProvider
+import acr.browser.lightning.utils.QUERY_PLACE_HOLDER
+import acr.browser.lightning.utils.smartUrlFilter
+import android.app.SearchManager
+import android.content.Intent
+import javax.inject.Inject
+
+/**
+ * Extracts data from an [Intent] and into a [BrowserContract.Action].
+ */
+class IntentExtractor @Inject constructor(private val searchEngineProvider: SearchEngineProvider) {
+
+ /**
+ * Extract the action from the [intent] or return null if no data was extracted.
+ */
+ fun extractUrlFromIntent(intent: Intent): BrowserContract.Action? {
+ return when (intent.action) {
+ INTENT_PANIC_TRIGGER -> BrowserContract.Action.Panic
+ Intent.ACTION_WEB_SEARCH ->
+ extractSearchFromIntent(intent)?.let(BrowserContract.Action::LoadUrl)
+ else -> intent.dataString?.let(BrowserContract.Action::LoadUrl)
+ }
+ }
+
+ private fun extractSearchFromIntent(intent: Intent): String? {
+ val query = intent.getStringExtra(SearchManager.QUERY)
+ val searchUrl = "${searchEngineProvider.provideSearchEngine().queryUrl}$QUERY_PLACE_HOLDER"
+
+ return if (query?.isNotBlank() == true) {
+ smartUrlFilter(query, true, searchUrl)
+ } else {
+ null
+ }
+ }
+
+ companion object {
+ private const val INTENT_PANIC_TRIGGER = "info.guardianproject.panic.action.TRIGGER"
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/SearchBoxDisplayChoice.kt b/app/src/main/java/acr/browser/lightning/browser/search/SearchBoxDisplayChoice.kt
similarity index 84%
rename from app/src/main/java/acr/browser/lightning/browser/SearchBoxDisplayChoice.kt
rename to app/src/main/java/acr/browser/lightning/browser/search/SearchBoxDisplayChoice.kt
index 0f35b6cc3..fa3d0b8d1 100644
--- a/app/src/main/java/acr/browser/lightning/browser/SearchBoxDisplayChoice.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/search/SearchBoxDisplayChoice.kt
@@ -1,4 +1,4 @@
-package acr.browser.lightning.browser
+package acr.browser.lightning.browser.search
import acr.browser.lightning.preference.IntEnum
diff --git a/app/src/main/java/acr/browser/lightning/browser/SearchBoxModel.kt b/app/src/main/java/acr/browser/lightning/browser/search/SearchBoxModel.kt
similarity index 97%
rename from app/src/main/java/acr/browser/lightning/browser/SearchBoxModel.kt
rename to app/src/main/java/acr/browser/lightning/browser/search/SearchBoxModel.kt
index 286e8f682..4cfc5826e 100644
--- a/app/src/main/java/acr/browser/lightning/browser/SearchBoxModel.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/search/SearchBoxModel.kt
@@ -1,4 +1,4 @@
-package acr.browser.lightning.browser
+package acr.browser.lightning.browser.search
import acr.browser.lightning.R
import acr.browser.lightning.preference.UserPreferences
diff --git a/app/src/main/java/acr/browser/lightning/browser/search/SearchListener.kt b/app/src/main/java/acr/browser/lightning/browser/search/SearchListener.kt
new file mode 100644
index 000000000..ffb44d77b
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/search/SearchListener.kt
@@ -0,0 +1,50 @@
+package acr.browser.lightning.browser.search
+
+import android.view.KeyEvent
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputMethodManager
+import android.widget.TextView
+
+/**
+ * Listens for actions on the search bar.
+ *
+ * @param onConfirm Invoked when the user has confirmed what they are searching for.
+ * @param inputMethodManager Used to manage keyboard visibility.
+ */
+class SearchListener(
+ private val onConfirm: () -> Unit,
+ private val inputMethodManager: InputMethodManager
+) : View.OnKeyListener, TextView.OnEditorActionListener {
+
+ override fun onKey(view: View, keyCode: Int, keyEvent: KeyEvent): Boolean {
+ if (keyEvent.action != KeyEvent.ACTION_UP) {
+ return false
+ }
+ return when (keyCode) {
+ KeyEvent.KEYCODE_ENTER -> {
+ onConfirm()
+ view.clearFocus()
+ inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
+ true
+ }
+ else -> false
+ }
+ }
+
+ override fun onEditorAction(view: TextView, actionId: Int, event: KeyEvent?): Boolean {
+ if (actionId == EditorInfo.IME_ACTION_GO
+ || actionId == EditorInfo.IME_ACTION_DONE
+ || actionId == EditorInfo.IME_ACTION_NEXT
+ || actionId == EditorInfo.IME_ACTION_SEND
+ || actionId == EditorInfo.IME_ACTION_SEARCH
+ || event?.action == KeyEvent.KEYCODE_ENTER
+ ) {
+ onConfirm()
+ view.clearFocus()
+ inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
+ return true
+ }
+ return false
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/activity/StyleRemovingTextWatcher.kt b/app/src/main/java/acr/browser/lightning/browser/search/StyleRemovingTextWatcher.kt
similarity index 93%
rename from app/src/main/java/acr/browser/lightning/browser/activity/StyleRemovingTextWatcher.kt
rename to app/src/main/java/acr/browser/lightning/browser/search/StyleRemovingTextWatcher.kt
index d8e95e26c..f45d68ab6 100644
--- a/app/src/main/java/acr/browser/lightning/browser/activity/StyleRemovingTextWatcher.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/search/StyleRemovingTextWatcher.kt
@@ -1,4 +1,4 @@
-package acr.browser.lightning.browser.activity
+package acr.browser.lightning.browser.search
import android.text.Editable
import android.text.TextWatcher
diff --git a/app/src/main/java/acr/browser/lightning/browser/tab/DefaultTabTitle.kt b/app/src/main/java/acr/browser/lightning/browser/tab/DefaultTabTitle.kt
new file mode 100644
index 000000000..afc040a22
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/tab/DefaultTabTitle.kt
@@ -0,0 +1,9 @@
+package acr.browser.lightning.browser.tab
+
+import javax.inject.Qualifier
+
+/**
+ * The default title of a tab
+ */
+@Qualifier
+annotation class DefaultTabTitle
diff --git a/app/src/main/java/acr/browser/lightning/browser/tab/DefaultUserAgent.kt b/app/src/main/java/acr/browser/lightning/browser/tab/DefaultUserAgent.kt
new file mode 100644
index 000000000..dbcc55a38
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/tab/DefaultUserAgent.kt
@@ -0,0 +1,9 @@
+package acr.browser.lightning.browser.tab
+
+import javax.inject.Qualifier
+
+/**
+ * The default user agent marker.
+ */
+@Qualifier
+annotation class DefaultUserAgent
diff --git a/app/src/main/java/acr/browser/lightning/browser/tab/DesktopTabRecyclerViewAdapter.kt b/app/src/main/java/acr/browser/lightning/browser/tab/DesktopTabRecyclerViewAdapter.kt
new file mode 100644
index 000000000..d9ec3a76a
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/tab/DesktopTabRecyclerViewAdapter.kt
@@ -0,0 +1,131 @@
+package acr.browser.lightning.browser.tab
+
+import acr.browser.lightning.R
+import acr.browser.lightning.extensions.desaturate
+import acr.browser.lightning.extensions.dimen
+import acr.browser.lightning.extensions.drawTrapezoid
+import acr.browser.lightning.extensions.inflater
+import acr.browser.lightning.extensions.tint
+import acr.browser.lightning.utils.ThemeUtils
+import acr.browser.lightning.utils.Utils
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.core.widget.TextViewCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+
+/**
+ * The adapter that renders tabs in the desktop form.
+ *
+ * @param onClick Invoked when the tab is clicked.
+ * @param onLongClick Invoked when the tab is long pressed.
+ * @param onCloseClick Invoked when the tab's close button is clicked.
+ */
+class DesktopTabRecyclerViewAdapter(
+ context: Context,
+ private val onClick: (Int) -> Unit,
+ private val onLongClick: (Int) -> Unit,
+ private val onCloseClick: (Int) -> Unit,
+) : ListAdapter(
+ object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: TabViewState, newItem: TabViewState): Boolean =
+ oldItem.id == newItem.id
+
+ override fun areContentsTheSame(oldItem: TabViewState, newItem: TabViewState): Boolean =
+ oldItem == newItem
+ }
+) {
+ private val backgroundTabDrawable: Drawable
+ private val foregroundTabDrawable: Drawable
+ private var foregroundLayout: LinearLayout? = null
+
+ init {
+ val backgroundColor =
+ Utils.mixTwoColors(ThemeUtils.getPrimaryColor(context), Color.BLACK, 0.75f)
+ val backgroundTabBitmap = Bitmap.createBitmap(
+ context.dimen(R.dimen.desktop_tab_width),
+ context.dimen(R.dimen.desktop_tab_height),
+ Bitmap.Config.ARGB_8888
+ ).also {
+ Canvas(it).drawTrapezoid(backgroundColor, true)
+ }
+ backgroundTabDrawable = BitmapDrawable(context.resources, backgroundTabBitmap)
+
+ val foregroundColor = ThemeUtils.getPrimaryColor(context)
+ val foregroundTabBitmap = Bitmap.createBitmap(
+ context.dimen(R.dimen.desktop_tab_width),
+ context.dimen(R.dimen.desktop_tab_height),
+ Bitmap.Config.ARGB_8888
+ ).also {
+ Canvas(it).drawTrapezoid(foregroundColor, false)
+ }
+ foregroundTabDrawable = BitmapDrawable(context.resources, foregroundTabBitmap).mutate()
+ }
+
+ fun updateForegroundTabColor(color: Int) {
+ foregroundTabDrawable.tint(color)
+ foregroundLayout?.invalidate()
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): TabViewHolder {
+ val view =
+ viewGroup.context.inflater.inflate(R.layout.tab_list_item_horizontal, viewGroup, false)
+ return TabViewHolder(
+ view,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ onCloseClick = onCloseClick
+ )
+ }
+
+ override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
+ holder.exitButton.tag = position
+
+ val tab = getItem(position)
+
+ holder.txtTitle.text = tab.title
+ updateViewHolderAppearance(holder, tab.isSelected)
+ updateViewHolderFavicon(holder, tab.icon, tab.isSelected)
+ updateViewHolderBackground(holder, tab.isSelected)
+ }
+
+ private fun updateViewHolderFavicon(
+ viewHolder: TabViewHolder,
+ favicon: Bitmap?,
+ isForeground: Boolean
+ ) {
+ favicon?.let {
+ if (isForeground) {
+ viewHolder.favicon.setImageBitmap(it)
+ } else {
+ viewHolder.favicon.setImageBitmap(it.desaturate())
+ }
+ } ?: viewHolder.favicon.setImageResource(R.drawable.ic_webpage)
+ }
+
+ private fun updateViewHolderBackground(viewHolder: TabViewHolder, isForeground: Boolean) {
+ if (isForeground) {
+ foregroundLayout = viewHolder.layout
+ viewHolder.layout.background = foregroundTabDrawable
+ } else {
+ viewHolder.layout.background = backgroundTabDrawable
+ }
+ }
+
+ private fun updateViewHolderAppearance(
+ viewHolder: TabViewHolder,
+ isForeground: Boolean
+ ) {
+ if (isForeground) {
+ TextViewCompat.setTextAppearance(viewHolder.txtTitle, R.style.boldText)
+ } else {
+ TextViewCompat.setTextAppearance(viewHolder.txtTitle, R.style.normalText)
+ }
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/tab/DrawerTabRecyclerViewAdapter.kt b/app/src/main/java/acr/browser/lightning/browser/tab/DrawerTabRecyclerViewAdapter.kt
new file mode 100644
index 000000000..f34b1dd56
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/tab/DrawerTabRecyclerViewAdapter.kt
@@ -0,0 +1,90 @@
+package acr.browser.lightning.browser.tab
+
+import acr.browser.lightning.R
+import acr.browser.lightning.browser.tab.view.BackgroundDrawable
+import acr.browser.lightning.extensions.desaturate
+import acr.browser.lightning.extensions.inflater
+import android.graphics.Bitmap
+import android.view.ViewGroup
+import androidx.core.widget.TextViewCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+
+/**
+ * The adapter that renders tabs in the drawer list form.
+ *
+ * @param onClick Invoked when the tab is clicked.
+ * @param onLongClick Invoked when the tab is long pressed.
+ * @param onCloseClick Invoked when the tab's close button is clicked.
+ */
+class DrawerTabRecyclerViewAdapter(
+ private val onClick: (Int) -> Unit,
+ private val onLongClick: (Int) -> Unit,
+ private val onCloseClick: (Int) -> Unit,
+) : ListAdapter(
+ object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: TabViewState, newItem: TabViewState): Boolean =
+ oldItem.id == newItem.id
+
+ override fun areContentsTheSame(oldItem: TabViewState, newItem: TabViewState): Boolean =
+ oldItem == newItem
+ }
+) {
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): TabViewHolder {
+ val view = viewGroup.context.inflater.inflate(R.layout.tab_list_item, viewGroup, false)
+ view.background = BackgroundDrawable(view.context)
+ return TabViewHolder(
+ view,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ onCloseClick = onCloseClick
+ )
+ }
+
+ override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
+ holder.exitButton.tag = position
+
+ val tab = getItem(position)
+
+ holder.txtTitle.text = tab.title
+ updateViewHolderAppearance(holder, tab.isSelected)
+ updateViewHolderFavicon(holder, tab.icon, tab.isSelected)
+ updateViewHolderBackground(holder, tab.isSelected)
+ }
+
+ private fun updateViewHolderFavicon(
+ viewHolder: TabViewHolder,
+ favicon: Bitmap?,
+ isForeground: Boolean
+ ) {
+ favicon?.let {
+ if (isForeground) {
+ viewHolder.favicon.setImageBitmap(it)
+ } else {
+ viewHolder.favicon.setImageBitmap(it.desaturate())
+ }
+ } ?: viewHolder.favicon.setImageResource(R.drawable.ic_webpage)
+ }
+
+ private fun updateViewHolderBackground(viewHolder: TabViewHolder, isForeground: Boolean) {
+ val verticalBackground = viewHolder.layout.background as BackgroundDrawable
+ verticalBackground.isCrossFadeEnabled = false
+ if (isForeground) {
+ verticalBackground.startTransition(200)
+ } else {
+ verticalBackground.reverseTransition(200)
+ }
+ }
+
+ private fun updateViewHolderAppearance(
+ viewHolder: TabViewHolder,
+ isForeground: Boolean
+ ) {
+ if (isForeground) {
+ TextViewCompat.setTextAppearance(viewHolder.txtTitle, R.style.boldText)
+ } else {
+ TextViewCompat.setTextAppearance(viewHolder.txtTitle, R.style.normalText)
+ }
+ }
+}
diff --git a/app/src/main/java/acr/browser/lightning/browser/RecentTabModel.kt b/app/src/main/java/acr/browser/lightning/browser/tab/RecentTabModel.kt
similarity index 82%
rename from app/src/main/java/acr/browser/lightning/browser/RecentTabModel.kt
rename to app/src/main/java/acr/browser/lightning/browser/tab/RecentTabModel.kt
index fec49c930..ecaf38866 100644
--- a/app/src/main/java/acr/browser/lightning/browser/RecentTabModel.kt
+++ b/app/src/main/java/acr/browser/lightning/browser/tab/RecentTabModel.kt
@@ -1,13 +1,14 @@
-package acr.browser.lightning.browser
+package acr.browser.lightning.browser.tab
import acr.browser.lightning.extensions.popIfNotEmpty
import android.os.Bundle
-import java.util.*
+import java.util.Stack
+import javax.inject.Inject
/**
* A model that saves [Bundle] and returns the last returned one.
*/
-class RecentTabModel {
+class RecentTabModel @Inject constructor() {
private val bundleStack: Stack = Stack()
diff --git a/app/src/main/java/acr/browser/lightning/browser/tab/TabAdapter.kt b/app/src/main/java/acr/browser/lightning/browser/tab/TabAdapter.kt
new file mode 100644
index 000000000..e33264241
--- /dev/null
+++ b/app/src/main/java/acr/browser/lightning/browser/tab/TabAdapter.kt
@@ -0,0 +1,223 @@
+package acr.browser.lightning.browser.tab
+
+import acr.browser.lightning.browser.download.PendingDownload
+import acr.browser.lightning.browser.proxy.Proxy
+import acr.browser.lightning.constant.DESKTOP_USER_AGENT
+import acr.browser.lightning.preference.UserPreferences
+import acr.browser.lightning.preference.userAgent
+import acr.browser.lightning.ssl.SslCertificateInfo
+import acr.browser.lightning.ssl.SslState
+import acr.browser.lightning.utils.Option
+import acr.browser.lightning.utils.value
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.view.View
+import android.webkit.WebView
+import androidx.activity.result.ActivityResult
+import io.reactivex.Observable
+import io.reactivex.subjects.PublishSubject
+
+/**
+ * Creates the adaptation between a [WebView] and the [TabModel] interface used by the browser.
+ */
+class TabAdapter(
+ tabInitializer: TabInitializer,
+ private val webView: WebView,
+ private val requestHeaders: Map,
+ private val tabWebViewClient: TabWebViewClient,
+ private val tabWebChromeClient: TabWebChromeClient,
+ private val userPreferences: UserPreferences,
+ private val defaultUserAgent: String,
+ private val defaultTabTitle: String,
+ private val iconFreeze: Bitmap,
+ private val proxy: Proxy
+) : TabModel {
+
+ private var latentInitializer: FreezableBundleInitializer? = null
+
+ private var findInPageQuery: String? = null
+ private var toggleDesktop: Boolean = false
+ private val downloadsSubject = PublishSubject.create()
+
+ init {
+ webView.webViewClient = tabWebViewClient
+ webView.webChromeClient = tabWebChromeClient
+ webView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
+ downloadsSubject.onNext(
+ PendingDownload(
+ url = url,
+ userAgent = userAgent,
+ contentDisposition = contentDisposition,
+ mimeType = mimetype,
+ contentLength = contentLength
+ )
+ )
+ }
+ if (tabInitializer is FreezableBundleInitializer) {
+ latentInitializer = tabInitializer
+ } else {
+ loadFromInitializer(tabInitializer)
+ }
+ }
+
+ override val id: Int = webView.id
+
+ override fun loadUrl(url: String) {
+ if (!proxy.isProxyReady()) return
+ webView.loadUrl(url, requestHeaders)
+ }
+
+ override fun loadFromInitializer(tabInitializer: TabInitializer) {
+ tabInitializer.initialize(webView, requestHeaders)
+ }
+
+ override fun goBack() {
+ webView.goBack()
+ }
+
+ override fun canGoBack(): Boolean = webView.canGoBack()
+
+ override fun canGoBackChanges(): Observable = tabWebViewClient.goBackObservable.hide()
+
+ override fun goForward() {
+ webView.goForward()
+ }
+
+ override fun canGoForward(): Boolean = webView.canGoForward()
+
+ override fun canGoForwardChanges(): Observable =
+ tabWebViewClient.goForwardObservable.hide()
+
+ override fun toggleDesktopAgent() {
+ if (!toggleDesktop) {
+ webView.settings.userAgentString = DESKTOP_USER_AGENT
+ } else {
+ webView.settings.userAgentString = userPreferences.userAgent(defaultUserAgent)
+
+ }
+
+ toggleDesktop = !toggleDesktop
+ }
+
+ override fun reload() {
+ if (!proxy.isProxyReady()) return
+ webView.reload()
+ }
+
+ override fun stopLoading() {
+ webView.stopLoading()
+ }
+
+ override fun find(query: String) {
+ webView.findAllAsync(query)
+ findInPageQuery = query
+ }
+
+ override fun findNext() {
+ webView.findNext(true)
+ }
+
+ override fun findPrevious() {
+ webView.findNext(false)
+ }
+
+ override fun clearFindMatches() {
+ webView.clearMatches()
+ findInPageQuery = null
+ }
+
+ override val findQuery: String?
+ get() = findInPageQuery
+
+ override val favicon: Bitmap?
+ get() = latentInitializer?.let { iconFreeze }
+ ?: tabWebChromeClient.faviconObservable.value?.value()
+
+ override fun faviconChanges(): Observable