diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 573820b0c..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "i2p.android.base"] - path = i2p.android.base - url = https://github.com/i2p/i2p.android.base diff --git a/CHANGELOG.md b/CHANGELOG.md index 289a9a31f..00ec5d63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ Change Log ========== +Version 5.1.0 *(2019-10-01)* +---------------------------- +- Made copy link action available in incognito mode. +- Fixed bug with bookmark and tab drawer transparency. +- Added feature to freeze old tabs until they are accessed if the app restarts. +- Added button to search suggestions layout that inserts the suggestion rather than searching for it. +- Added support for full sandboxed incognito mode on Android Pie (API 28) and up. + Version 5.0.2 *(2019-09-07)* ---------------------------- - Target Android 10 API 29 diff --git a/app/build.gradle b/app/build.gradle index 2115436f9..f56a21218 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,7 @@ android { defaultConfig { minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion - versionName "5.0.2" + versionName "5.1.0" vectorDrawables.useSupportLibrary = true } @@ -60,14 +60,14 @@ android { dimension "capabilities" buildConfigField "boolean", "FULL_VERSION", "Boolean.parseBoolean(\"true\")" applicationId "acr.browser.lightning" - versionCode 100 + versionCode 101 } lightningLite { dimension "capabilities" buildConfigField "boolean", "FULL_VERSION", "Boolean.parseBoolean(\"false\")" applicationId "acr.browser.barebones" - versionCode 101 + versionCode 102 } } @@ -144,11 +144,8 @@ dependencies { implementation 'com.anthonycr.grant:permissions:1.1.2' // proxy support - // TODO: Replace I2P submodule with version 0.9.41 when it is available on maven - // implementation 'net.i2p.android:client:0.9.40' - // implementation 'net.i2p.android:helper:0.9.5' - implementation project(':client') - implementation project(':helper') + implementation 'net.i2p.android:client:0.9.42' + implementation 'net.i2p.android:helper:0.9.5' implementation 'com.squareup.okhttp3:okhttp:3.12.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d238f67b7..07f408930 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -145,6 +145,7 @@ android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden|keyboard" android:label="@string/app_name" android:launchMode="singleInstance" + android:process=":incognito" android:parentActivityName=".MainActivity" android:theme="@style/Theme.DarkTheme" android:windowSoftInputMode="stateHidden|adjustResize"> diff --git a/app/src/main/java/acr/browser/lightning/BrowserApp.kt b/app/src/main/java/acr/browser/lightning/BrowserApp.kt index a3ab67ef3..c979c61ff 100644 --- a/app/src/main/java/acr/browser/lightning/BrowserApp.kt +++ b/app/src/main/java/acr/browser/lightning/BrowserApp.kt @@ -35,7 +35,7 @@ class BrowserApp : Application() { @Inject internal lateinit var logger: Logger @Inject internal lateinit var buildInfo: BuildInfo - val applicationComponent: AppComponent by lazy { appComponent } + lateinit var applicationComponent: AppComponent override fun attachBaseContext(base: Context) { super.attachBaseContext(base) @@ -57,6 +57,12 @@ class BrowserApp : Application() { .build()) } + if (Build.VERSION.SDK_INT >= 28) { + if (getProcessName() == "$packageName:incognito") { + WebView.setDataDirectorySuffix("incognito") + } + } + val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, ex -> @@ -78,7 +84,7 @@ class BrowserApp : Application() { } } - appComponent = DaggerAppComponent.builder() + applicationComponent = DaggerAppComponent.builder() .application(this) .buildInfo(createBuildInfo()) .build() @@ -117,16 +123,11 @@ class BrowserApp : Application() { }) companion object { - private const val TAG = "BrowserApp" init { AppCompatDelegate.setCompatVectorFromResourcesEnabled(Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) } - - @JvmStatic - lateinit var appComponent: AppComponent - } } diff --git a/app/src/main/java/acr/browser/lightning/Capabilities.kt b/app/src/main/java/acr/browser/lightning/Capabilities.kt new file mode 100644 index 000000000..5099f16be --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/Capabilities.kt @@ -0,0 +1,22 @@ +package acr.browser.lightning + +import android.os.Build + +/** + * Capabilities that are specific to certain API levels. + */ +enum class Capabilities { + FULL_INCOGNITO, + WEB_RTC, + THIRD_PARTY_COOKIE_BLOCKING +} + +/** + * Returns true if the capability is supported, false otherwise. + */ +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/IncognitoActivity.kt b/app/src/main/java/acr/browser/lightning/IncognitoActivity.kt index 7b6797eba..bf1eebb6c 100644 --- a/app/src/main/java/acr/browser/lightning/IncognitoActivity.kt +++ b/app/src/main/java/acr/browser/lightning/IncognitoActivity.kt @@ -20,7 +20,11 @@ class IncognitoActivity : BrowserActivity() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { CookieSyncManager.createInstance(this@IncognitoActivity) } - cookieManager.setAcceptCookie(userPreferences.incognitoCookiesEnabled) + if (Capabilities.FULL_INCOGNITO.isSupported) { + cookieManager.setAcceptCookie(userPreferences.cookiesEnabled) + } else { + cookieManager.setAcceptCookie(userPreferences.incognitoCookiesEnabled) + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -41,7 +45,7 @@ class IncognitoActivity : BrowserActivity() { override fun isIncognito() = true - override fun closeActivity() = closeDrawers(this::closeBrowser) + override fun closeActivity() = closeDrawers(::closeBrowser) companion object { /** 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 bd4fbabbf..cce00d941 100644 --- a/app/src/main/java/acr/browser/lightning/adblock/BloomFilterAdBlocker.kt +++ b/app/src/main/java/acr/browser/lightning/adblock/BloomFilterAdBlocker.kt @@ -130,51 +130,34 @@ class BloomFilterAdBlocker @Inject constructor( } override fun isAd(url: String): Boolean { - val domain = try { - getDomainName(url) - } catch (exception: URISyntaxException) { - logger.log(TAG, "URL '$url' is invalid", exception) - return false - } + val domain = url.host() ?: return false val mightBeOnBlockList = bloomFilter.mightContain(domain) - return if (mightBeOnBlockList) { - val isOnBlockList = hostsRepository.containsHost(domain) - if (isOnBlockList) { - logger.log(TAG, "URL '$url' is an ad") - } else { - logger.log(TAG, "False positive for $url") - } + return when { + mightBeOnBlockList -> { + val isOnBlockList = hostsRepository.containsHost(domain) + if (isOnBlockList) { + logger.log(TAG, "URL '$url' is an ad") + } else { + logger.log(TAG, "False positive for $url") + } - isOnBlockList - } else { - false + isOnBlockList + } + domain.name.startsWith("www.") -> isAd(domain.name.substring(4)) + else -> false } } /** - * Returns the probable domain name for a given URL - * - * @param url the url to parse - * @return returns the domain - * @throws URISyntaxException throws an exception if the string cannot form a URI + * Extract the [Host] from a [String] representing a URL. Returns null if no host was extracted. */ - @Throws(URISyntaxException::class) - private fun getDomainName(url: String): Host { - val host = url.indexOf('/', 8) - .takeIf { it != -1 } - ?.let(url::take) - ?: url - - val uri = URI(host) - val domain = uri.host ?: return Host(host) - - return Host(if (domain.startsWith("www.")) { - domain.substring(4) - } else { - domain - }) + private fun String.host(): Host? = try { + URI(this).host?.let(::Host) + } catch (exception: URISyntaxException) { + logger.log(TAG, "Invalid URL: $this", exception) + null } companion object { diff --git a/app/src/main/java/acr/browser/lightning/browser/SearchBoxModel.kt b/app/src/main/java/acr/browser/lightning/browser/SearchBoxModel.kt index 82af137fa..286e8f682 100644 --- a/app/src/main/java/acr/browser/lightning/browser/SearchBoxModel.kt +++ b/app/src/main/java/acr/browser/lightning/browser/SearchBoxModel.kt @@ -50,6 +50,6 @@ class SearchBoxModel @Inject constructor( } } - private fun safeDomain(url: String) = Utils.getDomainName(url) + private fun safeDomain(url: String) = Utils.getDisplayDomainName(url) } diff --git a/app/src/main/java/acr/browser/lightning/browser/TabsManager.kt b/app/src/main/java/acr/browser/lightning/browser/TabsManager.kt index e7eedcf5d..55246f9f1 100644 --- a/app/src/main/java/acr/browser/lightning/browser/TabsManager.kt +++ b/app/src/main/java/acr/browser/lightning/browser/TabsManager.kt @@ -1,5 +1,6 @@ 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 @@ -101,7 +102,7 @@ class TabsManager @Inject constructor( .subscribeOn(mainScheduler) .observeOn(databaseScheduler) .flatMapObservable { - return@flatMapObservable if (incognito) { + if (incognito) { initializeIncognitoMode(it.value()) } else { initializeRegularMode(it.value(), activity) @@ -116,9 +117,7 @@ class TabsManager @Inject constructor( * Returns an [Observable] that emits the [TabInitializer] for incognito mode. */ private fun initializeIncognitoMode(initialUrl: String?): Observable = - Observable.fromCallable { - return@fromCallable initialUrl?.let(::UrlInitializer) ?: homePageInitializer - } + Observable.fromCallable { initialUrl?.let(::UrlInitializer) ?: homePageInitializer } /** * Returns an [Observable] that emits the [TabInitializer] for normal operation mode. @@ -156,7 +155,7 @@ class TabsManager @Inject constructor( * saved on disk. Can potentially be empty. */ private fun restorePreviousTabs(): Observable = readSavedStateFromDisk() - .map { bundle -> + .map { (bundle, title) -> return@map bundle.getString(URL_KEY)?.let { url -> when { url.isBookmarkUrl() -> bookmarkPageInitializer @@ -165,7 +164,8 @@ class TabsManager @Inject constructor( url.isHistoryUrl() -> historyPageInitializer else -> homePageInitializer } - } ?: BundleInitializer(bundle) + } ?: FreezableBundleInitializer(bundle, title + ?: application.getString(R.string.tab_frozen)) } @@ -327,11 +327,11 @@ class TabsManager @Inject constructor( val outState = Bundle(ClassLoader.getSystemClassLoader()) logger.log(TAG, "Saving tab state") tabList - .filter { it.url.isNotBlank() } .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) @@ -354,15 +354,31 @@ class TabsManager @Inject constructor( * 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 + private fun readSavedStateFromDisk(): Observable> = Maybe .fromCallable { FileUtils.readBundleFromStorage(application, BUNDLE_STORAGE) } .flattenAsObservable { bundle -> bundle.keySet() .filter { it.startsWith(BUNDLE_KEY) } - .mapNotNull(bundle::getBundle) + .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. * @@ -409,6 +425,7 @@ class TabsManager @Inject constructor( 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/activity/BrowserActivity.kt b/app/src/main/java/acr/browser/lightning/browser/activity/BrowserActivity.kt index e6cb6deab..8804a33e9 100644 --- a/app/src/main/java/acr/browser/lightning/browser/activity/BrowserActivity.kt +++ b/app/src/main/java/acr/browser/lightning/browser/activity/BrowserActivity.kt @@ -9,6 +9,7 @@ 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 @@ -57,10 +58,6 @@ import android.os.Bundle import android.os.Handler import android.os.Message import android.provider.MediaStore -import android.text.Editable -import android.text.TextWatcher -import android.text.style.CharacterStyle -import android.text.style.ParagraphStyle import android.view.* import android.view.View.* import android.view.ViewGroup.LayoutParams @@ -159,6 +156,7 @@ abstract class BrowserActivity : ThemableBrowserActivity(), BrowserView, UIContr @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 @@ -986,25 +984,7 @@ abstract class BrowserActivity : ThemableBrowserActivity(), BrowserView, UIContr } protected fun performExitCleanUp() { - val currentTab = tabsManager.currentTab - if (userPreferences.clearCacheExit && currentTab != null && !isIncognito()) { - WebUtils.clearCache(currentTab.webView) - logger.log(TAG, "Cache Cleared") - } - if (userPreferences.clearHistoryExitEnabled && !isIncognito()) { - WebUtils.clearHistory(this, historyModel, databaseScheduler) - logger.log(TAG, "History Cleared") - } - if (userPreferences.clearCookiesExitEnabled && !isIncognito()) { - WebUtils.clearCookies(this) - logger.log(TAG, "Cookies Cleared") - } - if (userPreferences.clearWebStorageExitEnabled && !isIncognito()) { - WebUtils.clearWebStorage() - logger.log(TAG, "WebStorage Cleared") - } else if (isIncognito()) { - WebUtils.clearWebStorage() // We want to make sure incognito mode is secure - } + exitCleanup.cleanUp(tabsManager.currentTab?.webView, this) } override fun onConfigurationChanged(newConfig: Configuration) { @@ -1266,6 +1246,15 @@ abstract class BrowserActivity : ThemableBrowserActivity(), BrowserView, UIContr */ 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, @@ -1278,7 +1267,6 @@ abstract class BrowserActivity : ThemableBrowserActivity(), BrowserView, UIContr inputMethodManager.hideSoftInputFromWindow(getUrl.windowToken, 0) presenter?.onAutoCompleteItemPressed() } - getUrl.setAdapter(suggestionsAdapter) } 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 new file mode 100644 index 000000000..d3e7a474b --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/BasicIncognitoExitCleanup.kt @@ -0,0 +1,18 @@ +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 + +/** + * Exit cleanup that should run on API < 28 when the incognito instance is closed. This is + * 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) { + // 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 new file mode 100644 index 000000000..56bf8b76a --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/DelegatingExitCleanup.kt @@ -0,0 +1,26 @@ +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.isSupported +import android.webkit.WebView +import javax.inject.Inject + +/** + * Exit cleanup that determines which sort of cleanup to do at runtime. It determines which cleanup + * to perform based on the API version and whether we are in incognito mode or normal mode. + */ +class DelegatingExitCleanup @Inject constructor( + private val basicIncognitoExitCleanup: BasicIncognitoExitCleanup, + private val enhancedIncognitoExitCleanup: EnhancedIncognitoExitCleanup, + private val normalExitCleanup: NormalExitCleanup +) : ExitCleanup { + override fun cleanUp(webView: WebView?, context: BrowserActivity) { + when { + context is MainActivity -> normalExitCleanup.cleanUp(webView, context) + Capabilities.FULL_INCOGNITO.isSupported -> enhancedIncognitoExitCleanup.cleanUp(webView, context) + else -> basicIncognitoExitCleanup.cleanUp(webView, context) + } + } +} 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 new file mode 100644 index 000000000..eefd6c07e --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/EnhancedIncognitoExitCleanup.kt @@ -0,0 +1,28 @@ +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 javax.inject.Inject + +/** + * Exit cleanup that should be run when the incognito process is exited on API >= 28. This cleanup + * clears cookies and all web data, which can be done without affecting + */ +class EnhancedIncognitoExitCleanup @Inject constructor( + private val logger: Logger +) : ExitCleanup { + override fun cleanUp(webView: WebView?, context: BrowserActivity) { + WebUtils.clearCache(webView) + logger.log(TAG, "Cache Cleared") + WebUtils.clearCookies(context) + logger.log(TAG, "Cookies Cleared") + WebUtils.clearWebStorage() + logger.log(TAG, "WebStorage Cleared") + } + + companion object { + private const val TAG = "EnhancedIncognitoExitCleanup" + } +} 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 new file mode 100644 index 000000000..796fa2f28 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/ExitCleanup.kt @@ -0,0 +1,18 @@ +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 + * is closing. + */ +interface ExitCleanup { + + /** + * Clean up the instance of the browser with the provided [webView] and [context]. + */ + fun cleanUp(webView: WebView?, context: BrowserActivity) + +} 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 new file mode 100644 index 000000000..73b7a3d72 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/browser/cleanup/NormalExitCleanup.kt @@ -0,0 +1,44 @@ +package acr.browser.lightning.browser.cleanup + +import acr.browser.lightning.browser.activity.BrowserActivity +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 io.reactivex.Scheduler +import javax.inject.Inject + +/** + * Exit cleanup that should run whenever the main browser process is exiting. + */ +class NormalExitCleanup @Inject constructor( + private val userPreferences: UserPreferences, + private val logger: Logger, + private val historyDatabase: HistoryDatabase, + @DatabaseScheduler private val databaseScheduler: Scheduler +) : ExitCleanup { + override fun cleanUp(webView: WebView?, context: BrowserActivity) { + if (userPreferences.clearCacheExit) { + WebUtils.clearCache(webView) + logger.log(TAG, "Cache Cleared") + } + if (userPreferences.clearHistoryExitEnabled) { + WebUtils.clearHistory(context, historyDatabase, databaseScheduler) + logger.log(TAG, "History Cleared") + } + if (userPreferences.clearCookiesExitEnabled) { + WebUtils.clearCookies(context) + logger.log(TAG, "Cookies Cleared") + } + if (userPreferences.clearWebStorageExitEnabled) { + WebUtils.clearWebStorage() + logger.log(TAG, "WebStorage Cleared") + } + } + + companion object { + const val TAG = "NormalExitCleanup" + } +} diff --git a/app/src/main/java/acr/browser/lightning/browser/tabs/TabsDrawerView.kt b/app/src/main/java/acr/browser/lightning/browser/tabs/TabsDrawerView.kt index b648ea775..a385b67c2 100644 --- a/app/src/main/java/acr/browser/lightning/browser/tabs/TabsDrawerView.kt +++ b/app/src/main/java/acr/browser/lightning/browser/tabs/TabsDrawerView.kt @@ -30,6 +30,8 @@ class TabsDrawerView @JvmOverloads constructor( init { orientation = VERTICAL + isClickable = true + isFocusable = true context.inflater.inflate(R.layout.tab_drawer, this, true) actionBack = findViewById(R.id.action_back) actionForward = findViewById(R.id.action_forward) diff --git a/app/src/main/java/acr/browser/lightning/database/adblock/HostsDatabase.kt b/app/src/main/java/acr/browser/lightning/database/adblock/HostsDatabase.kt index 48330d922..9bb7b86ce 100644 --- a/app/src/main/java/acr/browser/lightning/database/adblock/HostsDatabase.kt +++ b/app/src/main/java/acr/browser/lightning/database/adblock/HostsDatabase.kt @@ -70,11 +70,12 @@ class HostsDatabase @Inject constructor( override fun containsHost(host: Host): Boolean { database.query( TABLE_HOSTS, - null, + arrayOf(KEY_ID), "$KEY_NAME=?", arrayOf(host.name), null, null, + null, "1" ).safeUse { return it.moveToFirst() diff --git a/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.kt b/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.kt index b7be5a6b5..c6fd3b160 100644 --- a/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.kt +++ b/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.kt @@ -70,7 +70,7 @@ class HistoryDatabase @Inject constructor( database.query( false, TABLE_HISTORY, - arrayOf(KEY_URL), + arrayOf(KEY_ID), "$KEY_URL = ?", arrayOf(url), null, diff --git a/app/src/main/java/acr/browser/lightning/di/AppBindsModule.kt b/app/src/main/java/acr/browser/lightning/di/AppBindsModule.kt index eee6e18fa..152f58cf2 100644 --- a/app/src/main/java/acr/browser/lightning/di/AppBindsModule.kt +++ b/app/src/main/java/acr/browser/lightning/di/AppBindsModule.kt @@ -6,6 +6,8 @@ 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 @@ -28,29 +30,32 @@ import dagger.Module interface AppBindsModule { @Binds - fun provideBookmarkModel(bookmarkDatabase: BookmarkDatabase): BookmarkRepository + fun bindsExitCleanup(delegatingExitCleanup: DelegatingExitCleanup): ExitCleanup @Binds - fun provideDownloadsModel(downloadsDatabase: DownloadsDatabase): DownloadsRepository + fun bindsBookmarkModel(bookmarkDatabase: BookmarkDatabase): BookmarkRepository @Binds - fun providesHistoryModel(historyDatabase: HistoryDatabase): HistoryRepository + fun bindsDownloadsModel(downloadsDatabase: DownloadsDatabase): DownloadsRepository @Binds - fun providesAdBlockAllowListModel(adBlockAllowListDatabase: AdBlockAllowListDatabase): AdBlockAllowListRepository + fun bindsHistoryModel(historyDatabase: HistoryDatabase): HistoryRepository @Binds - fun providesAllowListModel(sessionAllowListModel: SessionAllowListModel): AllowListModel + fun bindsAdBlockAllowListModel(adBlockAllowListDatabase: AdBlockAllowListDatabase): AdBlockAllowListRepository @Binds - fun providesSslWarningPreferences(sessionSslWarningPreferences: SessionSslWarningPreferences): SslWarningPreferences + fun bindsAllowListModel(sessionAllowListModel: SessionAllowListModel): AllowListModel @Binds - fun providesHostsDataSource(assetsHostsDataSource: AssetsHostsDataSource): HostsDataSource + fun bindsSslWarningPreferences(sessionSslWarningPreferences: SessionSslWarningPreferences): SslWarningPreferences @Binds - fun providesHostsRepository(hostsDatabase: HostsDatabase): HostsRepository + fun bindsHostsDataSource(assetsHostsDataSource: AssetsHostsDataSource): HostsDataSource @Binds - fun providesHostsDataSourceProvider(preferencesHostsDataSourceProvider: PreferencesHostsDataSourceProvider): HostsDataSourceProvider + fun bindsHostsRepository(hostsDatabase: HostsDatabase): HostsRepository + + @Binds + fun bindsHostsDataSourceProvider(preferencesHostsDataSourceProvider: PreferencesHostsDataSourceProvider): HostsDataSourceProvider } diff --git a/app/src/main/java/acr/browser/lightning/di/AppComponent.kt b/app/src/main/java/acr/browser/lightning/di/AppComponent.kt index 96bc45ea0..c2987ebc5 100644 --- a/app/src/main/java/acr/browser/lightning/di/AppComponent.kt +++ b/app/src/main/java/acr/browser/lightning/di/AppComponent.kt @@ -9,14 +9,12 @@ import acr.browser.lightning.browser.activity.ThemableBrowserActivity import acr.browser.lightning.browser.bookmarks.BookmarksDrawerView import acr.browser.lightning.device.BuildInfo import acr.browser.lightning.dialog.LightningDialogBuilder -import acr.browser.lightning.download.DownloadHandler import acr.browser.lightning.download.LightningDownloadListener 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.utils.ProxyUtils import acr.browser.lightning.view.LightningChromeClient import acr.browser.lightning.view.LightningView import acr.browser.lightning.view.LightningWebClient @@ -55,8 +53,6 @@ interface AppComponent { fun inject(app: BrowserApp) - fun inject(proxyUtils: ProxyUtils) - fun inject(activity: ReadingActivity) fun inject(webClient: LightningWebClient) @@ -75,8 +71,6 @@ interface AppComponent { fun inject(chromeClient: LightningChromeClient) - fun inject(downloadHandler: DownloadHandler) - fun inject(searchBoxModel: SearchBoxModel) fun inject(generalSettingsFragment: GeneralSettingsFragment) diff --git a/app/src/main/java/acr/browser/lightning/di/AppModule.kt b/app/src/main/java/acr/browser/lightning/di/AppModule.kt index da885839c..9cde54745 100644 --- a/app/src/main/java/acr/browser/lightning/di/AppModule.kt +++ b/app/src/main/java/acr/browser/lightning/di/AppModule.kt @@ -120,14 +120,18 @@ class AppModule { @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() } + } + private fun createInterceptorWithMaxCacheAge(maxCacheAgeSeconds: Long) = Interceptor { chain -> + chain.proceed(chain.request()).newBuilder() + .header("cache-control", "max-age=$maxCacheAgeSeconds, max-stale=$maxCacheAgeSeconds") + .build() } @Singleton @@ -135,19 +139,11 @@ class AppModule { @SuggestionsClient fun providesSuggestionsHttpClient(application: Application): Single = Single.fromCallable { val intervalDay = TimeUnit.DAYS.toSeconds(1) - - val rewriteCacheControlInterceptor = Interceptor { chain -> - val originalResponse = chain.proceed(chain.request()) - originalResponse.newBuilder() - .header("cache-control", "max-age=$intervalDay, max-stale=$intervalDay") - .build() - } - val suggestionsCache = File(application.cacheDir, "suggestion_responses") return@fromCallable OkHttpClient.Builder() .cache(Cache(suggestionsCache, FileUtils.megabytesToBytes(1))) - .addNetworkInterceptor(rewriteCacheControlInterceptor) + .addNetworkInterceptor(createInterceptorWithMaxCacheAge(intervalDay)) .build() }.cache() @@ -155,20 +151,12 @@ class AppModule { @Provides @HostsClient fun providesHostsHttpClient(application: Application): Single = Single.fromCallable { - val intervalDay = TimeUnit.DAYS.toSeconds(365) - - val rewriteCacheControlInterceptor = Interceptor { chain -> - val originalResponse = chain.proceed(chain.request()) - originalResponse.newBuilder() - .header("cache-control", "max-age=$intervalDay, max-stale=$intervalDay") - .build() - } - + val intervalYear = TimeUnit.DAYS.toSeconds(365) val suggestionsCache = File(application.cacheDir, "hosts_cache") return@fromCallable OkHttpClient.Builder() .cache(Cache(suggestionsCache, FileUtils.megabytesToBytes(5))) - .addNetworkInterceptor(rewriteCacheControlInterceptor) + .addNetworkInterceptor(createInterceptorWithMaxCacheAge(intervalYear)) .build() }.cache() diff --git a/app/src/main/java/acr/browser/lightning/di/DiExtensions.kt b/app/src/main/java/acr/browser/lightning/di/DiExtensions.kt index 44bae7af5..4532a3129 100644 --- a/app/src/main/java/acr/browser/lightning/di/DiExtensions.kt +++ b/app/src/main/java/acr/browser/lightning/di/DiExtensions.kt @@ -1,3 +1,5 @@ +@file:JvmName("Injector") + package acr.browser.lightning.di import acr.browser.lightning.BrowserApp diff --git a/app/src/main/java/acr/browser/lightning/download/DownloadHandler.java b/app/src/main/java/acr/browser/lightning/download/DownloadHandler.java index 30ae4c584..b55f4e059 100644 --- a/app/src/main/java/acr/browser/lightning/download/DownloadHandler.java +++ b/app/src/main/java/acr/browser/lightning/download/DownloadHandler.java @@ -23,7 +23,6 @@ import javax.inject.Inject; import javax.inject.Singleton; -import acr.browser.lightning.BrowserApp; import acr.browser.lightning.BuildConfig; import acr.browser.lightning.MainActivity; import acr.browser.lightning.R; @@ -57,16 +56,26 @@ public class DownloadHandler { private static final String COOKIE_REQUEST_HEADER = "Cookie"; - @Inject DownloadsRepository downloadsRepository; - @Inject DownloadManager downloadManager; - @Inject @DatabaseScheduler Scheduler databaseScheduler; - @Inject @NetworkScheduler Scheduler networkScheduler; - @Inject @MainScheduler Scheduler mainScheduler; - @Inject Logger logger; + private final DownloadsRepository downloadsRepository; + private final DownloadManager downloadManager; + private final Scheduler databaseScheduler; + private final Scheduler networkScheduler; + private final Scheduler mainScheduler; + private final Logger logger; @Inject - public DownloadHandler() { - BrowserApp.getAppComponent().inject(this); + public DownloadHandler(DownloadsRepository downloadsRepository, + DownloadManager downloadManager, + @DatabaseScheduler Scheduler databaseScheduler, + @NetworkScheduler Scheduler networkScheduler, + @MainScheduler Scheduler mainScheduler, + Logger logger) { + this.downloadsRepository = downloadsRepository; + this.downloadManager = downloadManager; + this.databaseScheduler = databaseScheduler; + this.networkScheduler = networkScheduler; + this.mainScheduler = mainScheduler; + this.logger = logger; } /** @@ -77,15 +86,15 @@ public DownloadHandler() { * @param url The full url to the content that should be downloaded * @param userAgent User agent of the downloading application. * @param contentDisposition Content-disposition http header, if present. - * @param mimetype The mimetype of the content reported by the server + * @param mimeType The mimeType of the content reported by the server * @param contentSize The size of the content */ public void onDownloadStart(@NonNull Activity context, @NonNull UserPreferences manager, @NonNull String url, String userAgent, - @Nullable String contentDisposition, String mimetype, @NonNull String contentSize) { + @Nullable String contentDisposition, String mimeType, @NonNull String contentSize) { logger.log(TAG, "DOWNLOAD: Trying to download from URL: " + url); logger.log(TAG, "DOWNLOAD: Content disposition: " + contentDisposition); - logger.log(TAG, "DOWNLOAD: Mimetype: " + mimetype); + logger.log(TAG, "DOWNLOAD: MimeType: " + mimeType); logger.log(TAG, "DOWNLOAD: User agent: " + userAgent); // if we're dealing wih A/V content that's not explicitly marked @@ -95,7 +104,7 @@ public void onDownloadStart(@NonNull Activity context, @NonNull UserPreferences // query the package manager to see if there's a registered handler // that matches. Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(url), mimetype); + intent.setDataAndType(Uri.parse(url), mimeType); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addCategory(Intent.CATEGORY_BROWSABLE); intent.setComponent(null); @@ -119,7 +128,7 @@ public void onDownloadStart(@NonNull Activity context, @NonNull UserPreferences } } } - onDownloadStartNoStream(context, manager, url, userAgent, contentDisposition, mimetype, contentSize); + onDownloadStartNoStream(context, manager, url, userAgent, contentDisposition, mimeType, contentSize); } // This is to work around the fact that java.net.URI throws Exceptions @@ -299,7 +308,7 @@ private void onDownloadStartNoStream(@NonNull final Activity context, @NonNull U } private static boolean isWriteAccessAvailable(@NonNull Uri fileUri) { - if (fileUri.getPath() == null){ + if (fileUri.getPath() == null) { return false; } File file = new File(fileUri.getPath()); diff --git a/app/src/main/java/acr/browser/lightning/download/LightningDownloadListener.java b/app/src/main/java/acr/browser/lightning/download/LightningDownloadListener.java index a5335333c..b7dbdb17f 100644 --- a/app/src/main/java/acr/browser/lightning/download/LightningDownloadListener.java +++ b/app/src/main/java/acr/browser/lightning/download/LightningDownloadListener.java @@ -16,9 +16,9 @@ import javax.inject.Inject; -import acr.browser.lightning.BrowserApp; import acr.browser.lightning.R; import acr.browser.lightning.database.downloads.DownloadsRepository; +import acr.browser.lightning.di.Injector; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.log.Logger; import acr.browser.lightning.preference.UserPreferences; @@ -37,7 +37,7 @@ public class LightningDownloadListener implements DownloadListener { @Inject Logger logger; public LightningDownloadListener(Activity context) { - BrowserApp.getAppComponent().inject(this); + Injector.getInjector(context).inject(this); mActivity = context; } diff --git a/app/src/main/java/acr/browser/lightning/icon/TabCountView.kt b/app/src/main/java/acr/browser/lightning/icon/TabCountView.kt index 742ea22bb..d528093ce 100644 --- a/app/src/main/java/acr/browser/lightning/icon/TabCountView.kt +++ b/app/src/main/java/acr/browser/lightning/icon/TabCountView.kt @@ -61,7 +61,7 @@ class TabCountView @JvmOverloads constructor( } override fun onDraw(canvas: Canvas) { - val text: String = if (count > 99) { + val text: String = if (count > MAX_DISPLAYABLE_NUMBER) { context.getString(R.string.infinity) } else { numberFormat.format(count) @@ -88,4 +88,8 @@ class TabCountView @JvmOverloads constructor( super.onDraw(canvas) } + companion object { + private const val MAX_DISPLAYABLE_NUMBER = 99 + } + } diff --git a/app/src/main/java/acr/browser/lightning/interpolator/BezierDecelerateInterpolator.kt b/app/src/main/java/acr/browser/lightning/interpolator/BezierDecelerateInterpolator.kt index cff74b2aa..fa44d9cee 100644 --- a/app/src/main/java/acr/browser/lightning/interpolator/BezierDecelerateInterpolator.kt +++ b/app/src/main/java/acr/browser/lightning/interpolator/BezierDecelerateInterpolator.kt @@ -1,19 +1,15 @@ package acr.browser.lightning.interpolator -import androidx.core.view.animation.PathInterpolatorCompat import android.view.animation.Interpolator +import androidx.core.view.animation.PathInterpolatorCompat /** - * Bezier decelerate curve similar to iOS. - * On Kitkat and below, it reverts to a - * decelerate interpolator. + * Smooth bezier curve interpolator. */ class BezierDecelerateInterpolator : Interpolator { - companion object { - private val PATH_INTERPOLATOR: Interpolator = PathInterpolatorCompat.create(0.25f, 0.1f, 0.25f, 1f) - } + private val interpolator = PathInterpolatorCompat.create(0.25f, 0.1f, 0.25f, 1f) - override fun getInterpolation(input: Float): Float = PATH_INTERPOLATOR.getInterpolation(input) + override fun getInterpolation(input: Float): Float = interpolator.getInterpolation(input) } diff --git a/app/src/main/java/acr/browser/lightning/reading/activity/ReadingActivity.java b/app/src/main/java/acr/browser/lightning/reading/activity/ReadingActivity.java index 0e8a33ff8..191cb3ec2 100644 --- a/app/src/main/java/acr/browser/lightning/reading/activity/ReadingActivity.java +++ b/app/src/main/java/acr/browser/lightning/reading/activity/ReadingActivity.java @@ -18,8 +18,8 @@ import javax.inject.Inject; -import acr.browser.lightning.BrowserApp; import acr.browser.lightning.R; +import acr.browser.lightning.di.Injector; import acr.browser.lightning.di.MainScheduler; import acr.browser.lightning.di.NetworkScheduler; import acr.browser.lightning.dialog.BrowserDialog; @@ -79,7 +79,7 @@ public static void launch(@NonNull Context context, @NonNull String url) { @Override protected void onCreate(Bundle savedInstanceState) { - BrowserApp.getAppComponent().inject(this); + Injector.getInjector(this).inject(this); overridePendingTransition(R.anim.slide_in_from_right, R.anim.fade_out_scale); mInvert = mUserPreferences.getInvertColors(); @@ -151,7 +151,7 @@ private boolean loadPage(@Nullable Intent intent) { return false; } if (getSupportActionBar() != null) { - getSupportActionBar().setTitle(Utils.getDomainName(mUrl)); + getSupportActionBar().setTitle(Utils.getDisplayDomainName(mUrl)); } mProgressDialog = new ProgressDialog(ReadingActivity.this); diff --git a/app/src/main/java/acr/browser/lightning/search/SuggestionViewHolder.kt b/app/src/main/java/acr/browser/lightning/search/SuggestionViewHolder.kt index 1b055a962..fbb438d1b 100644 --- a/app/src/main/java/acr/browser/lightning/search/SuggestionViewHolder.kt +++ b/app/src/main/java/acr/browser/lightning/search/SuggestionViewHolder.kt @@ -9,4 +9,5 @@ class SuggestionViewHolder(view: View) { val imageView: ImageView = view.findViewById(R.id.suggestionIcon) val titleView: TextView = view.findViewById(R.id.title) val urlView: TextView = view.findViewById(R.id.url) + val insertSuggestion: View = view.findViewById(R.id.complete_search) } diff --git a/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.kt b/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.kt index 8d272c6ae..ee5d94d96 100644 --- a/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.kt +++ b/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.kt @@ -52,6 +52,15 @@ class SuggestionsAdapter( private val bookmarkIcon = context.drawable(R.drawable.ic_bookmark) private var suggestionsRepository: SuggestionsRepository + /** + * The listener that is fired when the insert button on a [SearchSuggestion] is clicked. + */ + var onSuggestionInsertClick: ((WebPage) -> Unit)? = null + + private val onClick = View.OnClickListener { + onSuggestionInsertClick?.invoke(it.tag as WebPage) + } + private val layoutInflater = LayoutInflater.from(context) init { @@ -100,7 +109,6 @@ class SuggestionsAdapter( override fun getItemId(position: Int): Long = 0 override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val holder: SuggestionViewHolder val finalView: View @@ -126,6 +134,9 @@ class SuggestionsAdapter( holder.imageView.setImageDrawable(image) + holder.insertSuggestion.tag = webPage + holder.insertSuggestion.setOnClickListener(onClick) + return finalView } @@ -152,9 +163,9 @@ class SuggestionsAdapter( } private fun Observable.results(): Flowable> = this + .toFlowable(BackpressureStrategy.LATEST) .map { it.toString().toLowerCase(Locale.getDefault()).trim() } .filter(String::isNotEmpty) - .toFlowable(BackpressureStrategy.LATEST) .share() .compose { upstream -> val searchEntries = upstream diff --git a/app/src/main/java/acr/browser/lightning/settings/fragment/AdvancedSettingsFragment.kt b/app/src/main/java/acr/browser/lightning/settings/fragment/AdvancedSettingsFragment.kt index 5dbe56157..42f787916 100644 --- a/app/src/main/java/acr/browser/lightning/settings/fragment/AdvancedSettingsFragment.kt +++ b/app/src/main/java/acr/browser/lightning/settings/fragment/AdvancedSettingsFragment.kt @@ -1,11 +1,13 @@ package acr.browser.lightning.settings.fragment +import acr.browser.lightning.Capabilities import acr.browser.lightning.R import acr.browser.lightning.browser.SearchBoxDisplayChoice import acr.browser.lightning.constant.TEXT_ENCODINGS import acr.browser.lightning.di.injector import acr.browser.lightning.extensions.resizeAndShow import acr.browser.lightning.extensions.withSingleChoiceItems +import acr.browser.lightning.isSupported import acr.browser.lightning.preference.UserPreferences import acr.browser.lightning.view.RenderingMode import android.os.Bundle @@ -50,16 +52,31 @@ class AdvancedSettingsFragment : AbstractSettingsFragment() { onCheckChange = { userPreferences.popupsEnabled = it } ) - checkBoxPreference( - preference = SETTINGS_ENABLE_COOKIES, - isChecked = userPreferences.cookiesEnabled, - onCheckChange = { userPreferences.cookiesEnabled = it } + val incognitoCheckboxPreference = checkBoxPreference( + preference = SETTINGS_COOKIES_INCOGNITO, + isEnabled = !Capabilities.FULL_INCOGNITO.isSupported, + isChecked = if (Capabilities.FULL_INCOGNITO.isSupported) { + userPreferences.cookiesEnabled + } else { + userPreferences.incognitoCookiesEnabled + }, + summary = if (Capabilities.FULL_INCOGNITO.isSupported) { + getString(R.string.incognito_cookies_pie) + } else { + null + }, + onCheckChange = { userPreferences.incognitoCookiesEnabled = it } ) checkBoxPreference( - preference = SETTINGS_COOKIES_INCOGNITO, - isChecked = userPreferences.incognitoCookiesEnabled, - onCheckChange = { userPreferences.incognitoCookiesEnabled = it } + preference = SETTINGS_ENABLE_COOKIES, + isChecked = userPreferences.cookiesEnabled, + onCheckChange = { + userPreferences.cookiesEnabled = it + if (Capabilities.FULL_INCOGNITO.isSupported) { + incognitoCheckboxPreference.isChecked = it + } + } ) checkBoxPreference( diff --git a/app/src/main/java/acr/browser/lightning/settings/fragment/PrivacySettingsFragment.kt b/app/src/main/java/acr/browser/lightning/settings/fragment/PrivacySettingsFragment.kt index b8bfa2270..2843b6473 100644 --- a/app/src/main/java/acr/browser/lightning/settings/fragment/PrivacySettingsFragment.kt +++ b/app/src/main/java/acr/browser/lightning/settings/fragment/PrivacySettingsFragment.kt @@ -1,5 +1,6 @@ package acr.browser.lightning.settings.fragment +import acr.browser.lightning.Capabilities import acr.browser.lightning.R import acr.browser.lightning.database.history.HistoryRepository import acr.browser.lightning.di.DatabaseScheduler @@ -8,8 +9,8 @@ import acr.browser.lightning.di.injector import acr.browser.lightning.dialog.BrowserDialog import acr.browser.lightning.dialog.DialogItem import acr.browser.lightning.extensions.snackbar +import acr.browser.lightning.isSupported import acr.browser.lightning.preference.UserPreferences -import acr.browser.lightning.utils.ApiUtils import acr.browser.lightning.utils.WebUtils import acr.browser.lightning.view.LightningView import android.os.Bundle @@ -45,7 +46,7 @@ class PrivacySettingsFragment : AbstractSettingsFragment() { checkBoxPreference( preference = SETTINGS_THIRDPCOOKIES, isChecked = userPreferences.blockThirdPartyCookiesEnabled, - isEnabled = ApiUtils.doesSupportThirdPartyCookieBlocking(), + isEnabled = Capabilities.THIRD_PARTY_COOKIE_BLOCKING.isSupported, onCheckChange = { userPreferences.blockThirdPartyCookiesEnabled = it } ) @@ -81,22 +82,20 @@ class PrivacySettingsFragment : AbstractSettingsFragment() { checkBoxPreference( preference = SETTINGS_DONOTTRACK, - isChecked = userPreferences.doNotTrackEnabled && ApiUtils.doesSupportWebViewHeaders(), - isEnabled = ApiUtils.doesSupportWebViewHeaders(), + isChecked = userPreferences.doNotTrackEnabled, onCheckChange = { userPreferences.doNotTrackEnabled = it } ) checkBoxPreference( preference = SETTINGS_WEBRTC, - isChecked = userPreferences.webRtcEnabled && ApiUtils.doesSupportWebRtc(), - isEnabled = ApiUtils.doesSupportWebRtc(), + isChecked = userPreferences.webRtcEnabled && Capabilities.WEB_RTC.isSupported, + isEnabled = Capabilities.WEB_RTC.isSupported, onCheckChange = { userPreferences.webRtcEnabled = it } ) checkBoxPreference( preference = SETTINGS_IDENTIFYINGHEADERS, - isChecked = userPreferences.removeIdentifyingHeadersEnabled && ApiUtils.doesSupportWebViewHeaders(), - isEnabled = ApiUtils.doesSupportWebViewHeaders(), + isChecked = userPreferences.removeIdentifyingHeadersEnabled, summary = "${LightningView.HEADER_REQUESTED_WITH}, ${LightningView.HEADER_WAP_PROFILE}", onCheckChange = { userPreferences.removeIdentifyingHeadersEnabled = it } ) diff --git a/app/src/main/java/acr/browser/lightning/utils/ApiUtils.kt b/app/src/main/java/acr/browser/lightning/utils/ApiUtils.kt deleted file mode 100644 index e0a3dc058..000000000 --- a/app/src/main/java/acr/browser/lightning/utils/ApiUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package acr.browser.lightning.utils - -import android.os.Build - -/** - * Utils to determine the capabilities of the Android version used on the device. - */ -object ApiUtils { - - /** - * Returns true if the Android version supports custom headers in the WebView. - */ - @JvmStatic - fun doesSupportWebViewHeaders(): Boolean = true - - /** - * Returns true if the Android version supports WebRTC in the WebView. - */ - @JvmStatic - fun doesSupportWebRtc(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - - /** - * Returns true if the Android version supports blocking third party cookies in the WebView. - */ - @JvmStatic - fun doesSupportThirdPartyCookieBlocking(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - -} diff --git a/app/src/main/java/acr/browser/lightning/utils/ProxyUtils.java b/app/src/main/java/acr/browser/lightning/utils/ProxyUtils.java index 47cbdd5ca..d822e8bd6 100644 --- a/app/src/main/java/acr/browser/lightning/utils/ProxyUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/ProxyUtils.java @@ -38,13 +38,17 @@ public final class ProxyUtils { private static boolean sI2PHelperBound; private static boolean sI2PProxyInitialized; - @Inject UserPreferences mUserPreferences; - @Inject DeveloperPreferences mDeveloperPreferences; - @Inject I2PAndroidHelper mI2PHelper; + private final UserPreferences userPreferences; + private final DeveloperPreferences developerPreferences; + private final I2PAndroidHelper i2PAndroidHelper; @Inject - public ProxyUtils() { - BrowserApp.getAppComponent().inject(this); + public ProxyUtils(UserPreferences userPreferences, + DeveloperPreferences developerPreferences, + I2PAndroidHelper i2PAndroidHelper) { + this.userPreferences = userPreferences; + this.developerPreferences = developerPreferences; + this.i2PAndroidHelper = i2PAndroidHelper; } /* @@ -52,23 +56,23 @@ public ProxyUtils() { * proxying for this session */ public void checkForProxy(@NonNull final Activity activity) { - final ProxyChoice currentProxyChoice = mUserPreferences.getProxyChoice(); + final ProxyChoice currentProxyChoice = userPreferences.getProxyChoice(); final boolean orbotInstalled = OrbotHelper.isOrbotInstalled(activity); - boolean orbotChecked = mDeveloperPreferences.getCheckedForTor(); + boolean orbotChecked = developerPreferences.getCheckedForTor(); boolean orbot = orbotInstalled && !orbotChecked; - boolean i2pInstalled = mI2PHelper.isI2PAndroidInstalled(); - boolean i2pChecked = mDeveloperPreferences.getCheckedForI2P(); + boolean i2pInstalled = i2PAndroidHelper.isI2PAndroidInstalled(); + boolean i2pChecked = developerPreferences.getCheckedForI2P(); boolean i2p = i2pInstalled && !i2pChecked; // Do only once per install if (currentProxyChoice != ProxyChoice.NONE && (orbot || i2p)) { if (orbot) { - mDeveloperPreferences.setCheckedForTor(true); + developerPreferences.setCheckedForTor(true); } if (i2p) { - mDeveloperPreferences.setCheckedForI2P(true); + developerPreferences.setCheckedForI2P(true); } AlertDialog.Builder builder = new AlertDialog.Builder(activity); @@ -80,13 +84,13 @@ public void checkForProxy(@NonNull final Activity activity) { list.add(new Pair<>(proxyChoice, proxyChoices[proxyChoice.getValue()])); } builder.setTitle(activity.getResources().getString(R.string.http_proxy)); - AlertDialogExtensionsKt.withSingleChoiceItems(builder, list, mUserPreferences.getProxyChoice(), newProxyChoice -> { - mUserPreferences.setProxyChoice(newProxyChoice); + AlertDialogExtensionsKt.withSingleChoiceItems(builder, list, userPreferences.getProxyChoice(), newProxyChoice -> { + userPreferences.setProxyChoice(newProxyChoice); return Unit.INSTANCE; }); builder.setPositiveButton(activity.getResources().getString(R.string.action_ok), (dialog, which) -> { - if (mUserPreferences.getProxyChoice() != ProxyChoice.NONE) { + if (userPreferences.getProxyChoice() != ProxyChoice.NONE) { initializeProxy(activity); } }); @@ -94,13 +98,13 @@ public void checkForProxy(@NonNull final Activity activity) { DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { switch (which) { case DialogInterface.BUTTON_POSITIVE: - mUserPreferences.setProxyChoice(orbotInstalled + userPreferences.setProxyChoice(orbotInstalled ? ProxyChoice.ORBOT : ProxyChoice.I2P); initializeProxy(activity); break; case DialogInterface.BUTTON_NEGATIVE: - mUserPreferences.setProxyChoice(ProxyChoice.NONE); + userPreferences.setProxyChoice(ProxyChoice.NONE); break; } }; @@ -121,7 +125,7 @@ private void initializeProxy(@NonNull Activity activity) { String host; int port; - switch (mUserPreferences.getProxyChoice()) { + switch (userPreferences.getProxyChoice()) { case NONE: // We shouldn't be here return; @@ -134,16 +138,16 @@ private void initializeProxy(@NonNull Activity activity) { break; case I2P: sI2PProxyInitialized = true; - if (sI2PHelperBound && !mI2PHelper.isI2PAndroidRunning()) { - mI2PHelper.requestI2PAndroidStart(activity); + if (sI2PHelperBound && !i2PAndroidHelper.isI2PAndroidRunning()) { + i2PAndroidHelper.requestI2PAndroidStart(activity); } host = "localhost"; port = 4444; break; default: case MANUAL: - host = mUserPreferences.getProxyHost(); - port = mUserPreferences.getProxyPort(); + host = userPreferences.getProxyHost(); + port = userPreferences.getProxyPort(); break; } @@ -156,11 +160,11 @@ private void initializeProxy(@NonNull Activity activity) { } public boolean isProxyReady(@NonNull Activity activity) { - if (mUserPreferences.getProxyChoice() == ProxyChoice.I2P) { - if (!mI2PHelper.isI2PAndroidRunning()) { + if (userPreferences.getProxyChoice() == ProxyChoice.I2P) { + if (!i2PAndroidHelper.isI2PAndroidRunning()) { ActivityExtensions.snackbar(activity, R.string.i2p_not_running); return false; - } else if (!mI2PHelper.areTunnelsActive()) { + } else if (!i2PAndroidHelper.areTunnelsActive()) { ActivityExtensions.snackbar(activity, R.string.i2p_tunnels_not_ready); return false; } @@ -170,7 +174,7 @@ public boolean isProxyReady(@NonNull Activity activity) { } public void updateProxySettings(@NonNull Activity activity) { - if (mUserPreferences.getProxyChoice() != ProxyChoice.NONE) { + if (userPreferences.getProxyChoice() != ProxyChoice.NONE) { initializeProxy(activity); } else { try { @@ -184,17 +188,17 @@ public void updateProxySettings(@NonNull Activity activity) { } public void onStop() { - mI2PHelper.unbind(); + i2PAndroidHelper.unbind(); sI2PHelperBound = false; } public void onStart(final Activity activity) { - if (mUserPreferences.getProxyChoice() == ProxyChoice.I2P) { + if (userPreferences.getProxyChoice() == ProxyChoice.I2P) { // Try to bind to I2P Android - mI2PHelper.bind(() -> { + i2PAndroidHelper.bind(() -> { sI2PHelperBound = true; - if (sI2PProxyInitialized && !mI2PHelper.isI2PAndroidRunning()) - mI2PHelper.requestI2PAndroidStart(activity); + if (sI2PProxyInitialized && !i2PAndroidHelper.isI2PAndroidRunning()) + i2PAndroidHelper.requestI2PAndroidStart(activity); }); } } diff --git a/app/src/main/java/acr/browser/lightning/utils/Utils.java b/app/src/main/java/acr/browser/lightning/utils/Utils.java index 0e5791723..ef8487950 100644 --- a/app/src/main/java/acr/browser/lightning/utils/Utils.java +++ b/app/src/main/java/acr/browser/lightning/utils/Utils.java @@ -109,7 +109,7 @@ public static int dpToPx(float dp) { * HTTPS if the URL is an SSL supported URL. */ @NonNull - public static String getDomainName(@Nullable String url) { + public static String getDisplayDomainName(@Nullable String url) { if (url == null || url.isEmpty()) return ""; boolean ssl = URLUtil.isHttpsUrl(url); diff --git a/app/src/main/java/acr/browser/lightning/view/LightningView.kt b/app/src/main/java/acr/browser/lightning/view/LightningView.kt index 92145eef4..e522853a6 100644 --- a/app/src/main/java/acr/browser/lightning/view/LightningView.kt +++ b/app/src/main/java/acr/browser/lightning/view/LightningView.kt @@ -4,6 +4,8 @@ package acr.browser.lightning.view +import acr.browser.lightning.Capabilities +import acr.browser.lightning.R import acr.browser.lightning.constant.DESKTOP_USER_AGENT import acr.browser.lightning.controller.UIController import acr.browser.lightning.di.DatabaseScheduler @@ -11,6 +13,8 @@ import acr.browser.lightning.di.MainScheduler import acr.browser.lightning.di.injector import acr.browser.lightning.dialog.LightningDialogBuilder import acr.browser.lightning.download.LightningDownloadListener +import acr.browser.lightning.extensions.drawable +import acr.browser.lightning.isSupported import acr.browser.lightning.log.Logger import acr.browser.lightning.network.NetworkConnectivityModel import acr.browser.lightning.preference.UserPreferences @@ -34,6 +38,7 @@ import android.webkit.WebSettings import android.webkit.WebSettings.LayoutAlgorithm import android.webkit.WebView import androidx.collection.ArrayMap +import androidx.core.graphics.drawable.toBitmap import io.reactivex.Observable import io.reactivex.Scheduler import io.reactivex.Single @@ -65,9 +70,15 @@ class LightningView( * Getter for the [LightningViewTitle] of the current LightningView instance. * * @return a NonNull instance of LightningViewTitle + * @return a NonNull instance of LightningViewTitle */ val titleInfo: LightningViewTitle + /** + * A tab initializer that should be run when the view is first attached. + */ + private var latentTabInitializer: FreezableBundleInitializer? = null + /** * Gets the current WebView instance of the tab. * @@ -91,6 +102,12 @@ class LightningView( var isForegroundTab: Boolean = false set(isForeground) { field = isForeground + if (isForeground) { + webView?.let { + latentTabInitializer?.initialize(it, requestHeaders) + latentTabInitializer = null + } + } uiController.tabChanged(this) } /** @@ -221,7 +238,13 @@ class LightningView( } initializePreferences() - tabInitializer.initialize(tab, requestHeaders) + if (tabInitializer !is FreezableBundleInitializer) { + tabInitializer.initialize(tab, requestHeaders) + } else { + latentTabInitializer = tabInitializer + titleInfo.setTitle(tabInitializer.initialTitle) + titleInfo.setFavicon(activity.drawable(R.drawable.ic_frozen).toBitmap()) + } networkDisposable = networkConnectivityModel.connectivity() .observeOn(mainScheduler) @@ -327,12 +350,8 @@ class LightningView( } settings.blockNetworkImage = userPreferences.blockImagesEnabled - if (!isIncognito) { - // Modifying headers causes SEGFAULTS, so disallow multi window if headers are enabled. - settings.setSupportMultipleWindows(userPreferences.popupsEnabled && !modifiesHeaders) - } else { - settings.setSupportMultipleWindows(false) - } + // Modifying headers causes SEGFAULTS, so disallow multi window if headers are enabled. + settings.setSupportMultipleWindows(userPreferences.popupsEnabled && !modifiesHeaders) settings.useWideViewPort = userPreferences.useWideViewPortEnabled settings.loadWithOverviewMode = userPreferences.overviewModeEnabled @@ -368,11 +387,11 @@ class LightningView( mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW } - if (!isIncognito) { + if (!isIncognito || Capabilities.FULL_INCOGNITO.isSupported) { domStorageEnabled = true setAppCacheEnabled(true) - cacheMode = WebSettings.LOAD_DEFAULT databaseEnabled = true + cacheMode = WebSettings.LOAD_DEFAULT } else { domStorageEnabled = false setAppCacheEnabled(false) @@ -435,16 +454,17 @@ class LightningView( /** * Save the state of the tab and return it as a [Bundle]. */ - fun saveState(): Bundle = Bundle(ClassLoader.getSystemClassLoader()).also { - webView?.saveState(it) - } + fun saveState(): Bundle = latentTabInitializer?.bundle + ?: Bundle(ClassLoader.getSystemClassLoader()).also { + webView?.saveState(it) + } /** * Pause the current WebView instance. */ fun onPause() { webView?.onPause() - logger.log(TAG, "WebView onPause: " + webView?.id) + logger.log(TAG, "WebView onPause: ${webView?.id}") } /** @@ -452,7 +472,7 @@ class LightningView( */ fun onResume() { webView?.onResume() - logger.log(TAG, "WebView onResume: " + webView?.id) + logger.log(TAG, "WebView onResume: ${webView?.id}") } /** diff --git a/app/src/main/java/acr/browser/lightning/view/LightningWebClient.kt b/app/src/main/java/acr/browser/lightning/view/LightningWebClient.kt index 05c3cdedc..200354e24 100644 --- a/app/src/main/java/acr/browser/lightning/view/LightningWebClient.kt +++ b/app/src/main/java/acr/browser/lightning/view/LightningWebClient.kt @@ -15,7 +15,10 @@ import acr.browser.lightning.log.Logger import acr.browser.lightning.preference.UserPreferences import acr.browser.lightning.ssl.SslState import acr.browser.lightning.ssl.SslWarningPreferences -import acr.browser.lightning.utils.* +import acr.browser.lightning.utils.IntentUtils +import acr.browser.lightning.utils.ProxyUtils +import acr.browser.lightning.utils.Utils +import acr.browser.lightning.utils.isSpecialUrl import android.annotation.TargetApi import android.app.Activity import android.content.ActivityNotFoundException @@ -297,11 +300,10 @@ class LightningWebClient( } return when { headers.isEmpty() -> false - ApiUtils.doesSupportWebViewHeaders() -> { + else -> { webView.loadUrl(url, headers) true } - else -> false } } diff --git a/app/src/main/java/acr/browser/lightning/view/TabInitializer.kt b/app/src/main/java/acr/browser/lightning/view/TabInitializer.kt index 54e83ec64..23ce5a687 100644 --- a/app/src/main/java/acr/browser/lightning/view/TabInitializer.kt +++ b/app/src/main/java/acr/browser/lightning/view/TabInitializer.kt @@ -144,7 +144,7 @@ class ResultMessageInitializer(private val resultMessage: Message) : TabInitiali /** * An initializer that restores the [WebView] state using the [bundle]. */ -class BundleInitializer(private val bundle: Bundle) : TabInitializer { +open class BundleInitializer(private val bundle: Bundle) : TabInitializer { override fun initialize(webView: WebView, headers: Map) { webView.restoreState(bundle) @@ -152,6 +152,15 @@ class BundleInitializer(private val bundle: Bundle) : TabInitializer { } +/** + * An initializer that can be delayed until the view is attached. [initialTitle] is the title that + * should be initially set on the tab. + */ +class FreezableBundleInitializer( + val bundle: Bundle, + val initialTitle: String +) : BundleInitializer(bundle) + /** * An initializer that does not load anything into the [WebView]. */ diff --git a/app/src/main/res/drawable/ic_frozen.xml b/app/src/main/res/drawable/ic_frozen.xml new file mode 100644 index 000000000..ce047caa5 --- /dev/null +++ b/app/src/main/res/drawable/ic_frozen.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_insert.xml b/app/src/main/res/drawable/ic_insert.xml new file mode 100644 index 000000000..7b1e33748 --- /dev/null +++ b/app/src/main/res/drawable/ic_insert.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/bookmark_drawer.xml b/app/src/main/res/layout/bookmark_drawer.xml index 9ba6dd078..cd64279f2 100644 --- a/app/src/main/res/layout/bookmark_drawer.xml +++ b/app/src/main/res/layout/bookmark_drawer.xml @@ -3,6 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" + android:clickable="true" + android:focusable="true" android:orientation="vertical"> diff --git a/app/src/main/res/layout/toolbar_content.xml b/app/src/main/res/layout/toolbar_content.xml index 4e7c5d103..e1035788f 100644 --- a/app/src/main/res/layout/toolbar_content.xml +++ b/app/src/main/res/layout/toolbar_content.xml @@ -18,7 +18,8 @@ android:id="@+id/home_image_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center" /> + android:layout_gravity="center" + android:contentDescription="@string/home" /> + android:paddingBottom="3dp" + android:weightSum="1"> @@ -43,4 +46,13 @@ android:textColor="?attr/autoCompleteUrlColor" /> + + diff --git a/app/src/main/res/menu-large/incognito.xml b/app/src/main/res/menu-large/incognito.xml index 19afb5f02..aa5518a55 100644 --- a/app/src/main/res/menu-large/incognito.xml +++ b/app/src/main/res/menu-large/incognito.xml @@ -27,6 +27,9 @@ + @@ -37,4 +40,4 @@ android:id="@+id/action_reading_mode" android:title="@string/reading_mode"/> - \ No newline at end of file + diff --git a/app/src/main/res/menu-xlarge/incognito.xml b/app/src/main/res/menu-xlarge/incognito.xml index 19afb5f02..aa5518a55 100644 --- a/app/src/main/res/menu-xlarge/incognito.xml +++ b/app/src/main/res/menu-xlarge/incognito.xml @@ -27,6 +27,9 @@ + @@ -37,4 +40,4 @@ android:id="@+id/action_reading_mode" android:title="@string/reading_mode"/> - \ No newline at end of file + diff --git a/app/src/main/res/menu/incognito.xml b/app/src/main/res/menu/incognito.xml index f83616473..a04fae04f 100644 --- a/app/src/main/res/menu/incognito.xml +++ b/app/src/main/res/menu/incognito.xml @@ -10,6 +10,9 @@ + @@ -21,4 +24,4 @@ android:id="@+id/action_reading_mode" android:title="@string/reading_mode"/> - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0523a67de..f443e0c05 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -95,6 +95,7 @@ USB 存储不可用 存储设备正忙。要允许下载,请轻触通知中的“关闭 USB 存储” 在无痕模式下启用 Cookie + 无痕模式运行于单独的进程,并在会话结束后重置。无痕模式下的 Cookie 设置由常规的 Cookie 选项控制。 Adobe Flash 手动 自动 @@ -315,5 +316,15 @@ 从广告屏蔽来源加载 Hosts 失败 - + + 颁发者 + 颁发给 + 颁发时间 + 过期时间 + + + 已冻结… + + + 插入搜索建议 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86c69bb71..b05abdb9e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -96,6 +96,7 @@ USB storage unavailable The storage is busy. To allow downloads, touch \'Turn Off USB Storage\' in the notification. Enable cookies in incognito mode + Incognito mode runs in a separate process that is reset after each session. Incognito cookie preferences are now controlled by the regular cookie preferences. Adobe Flash Manual Auto @@ -338,4 +339,10 @@ \'%1$s\' + + + Frozen… + + + Insert suggestion diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 86482dc72..0b9838270 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/i2p.android.base b/i2p.android.base deleted file mode 160000 index f3d1e8900..000000000 --- a/i2p.android.base +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f3d1e89002fb0e2cf284a4726b0a98cb437bcf84 diff --git a/settings.gradle b/settings.gradle index 206ff0edf..e7b4def49 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1 @@ include ':app' - -// TODO: Remove :client and :helper projects when 0.9.41 or later of I2P is available on maven -include ':client' -project(':client').projectDir = new File('i2p.android.base/lib/client') - -include ':helper' -project(':helper').projectDir = new File('i2p.android.base/lib/helper')