├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── generic │ └── .gitignore ├── gradlew ├── gradlew.bat ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── extensions │ │ │ └── generic │ │ │ │ ├── background.js │ │ │ │ ├── generic.js │ │ │ │ ├── home.js │ │ │ │ └── manifest.json │ │ ├── generic_injects.js │ │ └── pages │ │ │ ├── error.html │ │ │ ├── ic_warning.svg │ │ │ └── warning-certificate.html │ ├── java │ │ └── com │ │ │ └── phlox │ │ │ └── tvwebbrowser │ │ │ ├── Config.kt │ │ │ ├── TVBro.kt │ │ │ ├── activity │ │ │ ├── IncognitoModeMainActivity.kt │ │ │ ├── downloads │ │ │ │ ├── ActiveDownloadsModel.kt │ │ │ │ ├── DownloadListAdapter.kt │ │ │ │ ├── DownloadListItemView.kt │ │ │ │ ├── DownloadsActivity.kt │ │ │ │ └── DownloadsHistoryModel.kt │ │ │ ├── history │ │ │ │ ├── HistoryActivity.kt │ │ │ │ ├── HistoryAdapter.kt │ │ │ │ ├── HistoryItemView.kt │ │ │ │ └── HistoryModel.kt │ │ │ └── main │ │ │ │ ├── AdblockModel.kt │ │ │ │ ├── AutoUpdateModel.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainActivityViewModel.kt │ │ │ │ ├── SettingsModel.kt │ │ │ │ ├── TabsModel.kt │ │ │ │ ├── dialogs │ │ │ │ ├── SearchEngineConfigDialogFactory.kt │ │ │ │ ├── ShortcutDialog.kt │ │ │ │ ├── favorites │ │ │ │ │ ├── FavoriteEditorDialog.kt │ │ │ │ │ ├── FavoriteItemView.kt │ │ │ │ │ ├── FavoritesDialog.kt │ │ │ │ │ └── FavoritesListAdapter.kt │ │ │ │ └── settings │ │ │ │ │ ├── MainSettingsView.kt │ │ │ │ │ ├── SettingsDialog.kt │ │ │ │ │ ├── ShortcutsSettingsView.kt │ │ │ │ │ └── VersionSettingsView.kt │ │ │ │ └── view │ │ │ │ ├── ActionBar.kt │ │ │ │ ├── CursorLayout.kt │ │ │ │ └── tabs │ │ │ │ ├── TabsAdapter.kt │ │ │ │ ├── TabsDiffUtillCallback.kt │ │ │ │ └── TabsView.kt │ │ │ ├── model │ │ │ ├── Download.kt │ │ │ ├── FavoriteItem.kt │ │ │ ├── HistoryItem.kt │ │ │ ├── HomePageLink.kt │ │ │ ├── HostConfig.kt │ │ │ ├── WebTabState.kt │ │ │ ├── dao │ │ │ │ ├── DownloadDao.kt │ │ │ │ ├── FavoritesDao.kt │ │ │ │ ├── HistoryDao.kt │ │ │ │ ├── HostsDao.kt │ │ │ │ └── TabsDao.kt │ │ │ └── util │ │ │ │ └── Converters.kt │ │ │ ├── service │ │ │ └── downloads │ │ │ │ ├── DownloadService.kt │ │ │ │ └── DownloadTask.kt │ │ │ ├── singleton │ │ │ ├── AppDatabase.kt │ │ │ ├── FaviconsPool.kt │ │ │ └── shortcuts │ │ │ │ ├── Shortcut.kt │ │ │ │ └── ShortcutMgr.kt │ │ │ ├── utils │ │ │ ├── AndroidBug5497Workaround.kt │ │ │ ├── BaseAnimationListener.kt │ │ │ ├── DownloadUtils.kt │ │ │ ├── Extensions.kt │ │ │ ├── FaviconExtractor.kt │ │ │ ├── FileUtils.kt │ │ │ ├── LogUtils.kt │ │ │ ├── StringUtils.kt │ │ │ ├── UpdateChecker.kt │ │ │ ├── Utils.kt │ │ │ ├── VoiceSearchHelper.kt │ │ │ ├── activemodel │ │ │ │ ├── ActiveModel.kt │ │ │ │ └── ActiveModelsRepository.kt │ │ │ └── observable │ │ │ │ ├── ObservableList.kt │ │ │ │ └── SimpleObservable.kt │ │ │ ├── webengine │ │ │ ├── WebEngine.kt │ │ │ ├── WebEngineFactory.kt │ │ │ ├── WebEngineWindowProviderCallback.kt │ │ │ ├── gecko │ │ │ │ ├── GeckoViewEx.kt │ │ │ │ ├── GeckoViewWithVirtualCursor.kt │ │ │ │ ├── GeckoWebEngine.kt │ │ │ │ ├── HomePageHelper.kt │ │ │ │ └── delegates │ │ │ │ │ ├── AppWebExtensionBackgroundPortDelegate.kt │ │ │ │ │ ├── AppWebExtensionPortDelegate.kt │ │ │ │ │ ├── MyContentBlockingDelegate.kt │ │ │ │ │ ├── MyContentDelegate.kt │ │ │ │ │ ├── MyHistoryDelegate.kt │ │ │ │ │ ├── MyMediaSessionDelegate.kt │ │ │ │ │ ├── MyNavigationDelegate.kt │ │ │ │ │ ├── MyPermissionDelegate.kt │ │ │ │ │ ├── MyProgressDelegate.kt │ │ │ │ │ └── MyPromptDelegate.kt │ │ │ └── webview │ │ │ │ ├── AndroidJSInterface.kt │ │ │ │ ├── HomePageHelper.kt │ │ │ │ ├── Scripts.kt │ │ │ │ ├── WebViewEx.kt │ │ │ │ └── WebViewWebEngine.kt │ │ │ └── widgets │ │ │ ├── CheckableContainer.kt │ │ │ ├── CheckableImageButton.kt │ │ │ ├── NotificationView.kt │ │ │ └── SegmentedButtonTabsAdapter.kt │ └── res │ │ ├── anim │ │ ├── highlight_by_scale_anim.xml │ │ ├── infinite_fadeinout_anim.xml │ │ ├── right_menu_in_anim.xml │ │ └── right_menu_out_anim.xml │ │ ├── color │ │ └── webtab_horizontal_text_selector.xml │ │ ├── drawable-hdpi │ │ ├── ic_launcher.png │ │ └── tab_patch.9.png │ │ ├── drawable-mdpi │ │ ├── ic_launcher.png │ │ └── tab_patch.9.png │ │ ├── drawable-night-hdpi │ │ └── tab_patch.9.png │ │ ├── drawable-night-mdpi │ │ └── tab_patch.9.png │ │ ├── drawable-night-xhdpi │ │ └── tab_patch.9.png │ │ ├── drawable-night-xxhdpi │ │ └── tab_patch.9.png │ │ ├── drawable-night-xxxhdpi │ │ └── tab_patch.9.png │ │ ├── drawable-xhdpi │ │ ├── banner.png │ │ ├── ic_launcher.png │ │ └── tab_patch.9.png │ │ ├── drawable-xxhdpi │ │ ├── ic_launcher.png │ │ └── tab_patch.9.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_launcher.png │ │ └── tab_patch.9.png │ │ ├── drawable │ │ ├── back_icon_selector.xml │ │ ├── button_bg_selector.xml │ │ ├── favorite_item_view_bg_selector.xml │ │ ├── forward_icon_selector.xml │ │ ├── gray_badge_bg.xml │ │ ├── ic_adblock_off.xml │ │ ├── ic_adblock_on.xml │ │ ├── ic_arrow_back_grey_400_24dp.xml │ │ ├── ic_arrow_back_grey_900_24dp.xml │ │ ├── ic_arrow_forward_grey_400_24dp.xml │ │ ├── ic_arrow_forward_grey_900_24dp.xml │ │ ├── ic_baseline_add_box_24.xml │ │ ├── ic_block_popups.xml │ │ ├── ic_close_grey_900_24dp.xml │ │ ├── ic_close_grey_900_36dp.xml │ │ ├── ic_delete_grey_400_24dp.xml │ │ ├── ic_delete_grey_900_36dp.xml │ │ ├── ic_file_download_grey_900.xml │ │ ├── ic_history_grey_900_36dp.xml │ │ ├── ic_home_grey_900_24dp.xml │ │ ├── ic_incognito.xml │ │ ├── ic_keyboard_arrow_right_grey_900_18dp.xml │ │ ├── ic_menu_grey_900_36dp.xml │ │ ├── ic_mic_none_grey_900_36dp.xml │ │ ├── ic_mode_edit_grey_400_18dp.xml │ │ ├── ic_not_available.xml │ │ ├── ic_refresh_grey_900_24dp.xml │ │ ├── ic_settings_grey_900_24dp.xml │ │ ├── ic_star_border_grey_900_36dp.xml │ │ ├── ic_zoom_in_black_24dp.xml │ │ ├── ic_zoom_in_gray_24dp.xml │ │ ├── ic_zoom_out_black_24dp.xml │ │ ├── ic_zoom_out_gray_24dp.xml │ │ ├── list_item_bg_selector.xml │ │ ├── settings_tab_bg_selector.xml │ │ ├── tab_button_bg_selector.xml │ │ ├── text_link_background_selector.xml │ │ ├── webtab_horizontal_bkg.xml │ │ ├── webtab_horizontal_bkg_selector.xml │ │ ├── zoomin_icon_selector.xml │ │ └── zoomout_icon_selector.xml │ │ ├── layout │ │ ├── activity_downloads.xml │ │ ├── activity_history.xml │ │ ├── activity_main.xml │ │ ├── dialog_favorites.xml │ │ ├── dialog_new_favorite_item.xml │ │ ├── dialog_search_engine.xml │ │ ├── dialog_settings.xml │ │ ├── dialog_shortcut.xml │ │ ├── view_actionbar.xml │ │ ├── view_download_item.xml │ │ ├── view_favorite_item.xml │ │ ├── view_history_header_item.xml │ │ ├── view_history_item.xml │ │ ├── view_horizontal_webtab_item.xml │ │ ├── view_notification.xml │ │ ├── view_settings_main.xml │ │ ├── view_settings_version.xml │ │ ├── view_shortcut.xml │ │ ├── view_speach_recognizer_results.xml │ │ └── view_tabs.xml │ │ ├── menu │ │ └── menu_link.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-fa │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-iw │ │ └── strings.xml │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-pl │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-uk │ │ └── strings.xml │ │ ├── values-vi │ │ └── strings.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values-zh │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ └── provider_paths.xml │ └── test │ └── java │ └── com │ └── phlox │ └── tvwebbrowser │ └── utils │ └── FaviconExtractorTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── latest_version.json ├── metadata └── en-US │ ├── changelogs │ ├── 56.txt │ ├── 57.txt │ └── 59.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ ├── phoneScreenshots │ │ ├── 1.png │ │ └── 2.png │ ├── tvBanner.png │ └── tvScreenshots │ │ ├── 1.png │ │ └── 2.png │ ├── short_description.txt │ └── title.txt └── settings.gradle.kts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: truefedex 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS version: [e.g. Android TV 10] 28 | - Device [e.g. NVidia Shield Pro 2015] 29 | - TV Bro version [e.g. v1.8.1] 30 | - URL or domain name where problem can be reproduced [optional, e.g. https://google.com] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: truefedex 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | *.keystore 9 | *.hprof 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### License ### 2 | 3 | Copyright (c) 2019, Fedir Tsapana 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must follow one of two paths: 13 | 1. __Unmodified binary redistribution__ possible if and only if it binary identical 14 | to one of releases provided by author (Fedir Tsapana) published only at 15 | https://github.com/truefedex/tv-bro/releases web page. 16 | 2. __Modified binary redistribution__ possible but derived application name cannot 17 | contain "TV Bro" string in any case. It also must not use the same app icon and 18 | [application id](https://developer.android.com/studio/build/application-id) 19 | that used by original app version. Any modified binary redistribution should 20 | contain somewhere in GUI an about window where should be stated that it uses 21 | TV Bro's sources and link to https://github.com/truefedex/tv-bro. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TV Bro 2 | 3 | Simple web browser optimized to use with TV remote 4 | 5 | Features: 6 | - working with TV remote 7 | - tabs and bookmarks support 8 | - voice search support 9 | - switch user agent support 10 | - use Android builtin web rendering engine (WebKit/Blink based) 11 | - built-in download manager 12 | - browsing history 13 | - shortcuts 14 | 15 | Discussion pages: 16 | - https://forum.xda-developers.com/android/apps-games/tv-bro-browser-android-based-tvs-t3545295 17 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /app-release.apk 3 | /google-services.json 4 | /crashlytics/ 5 | /local.properties 6 | /crashlyticsGoogle/ 7 | /schemas/ 8 | /google/ 9 | -------------------------------------------------------------------------------- /app/generic/.gitignore: -------------------------------------------------------------------------------- 1 | /release/ 2 | -------------------------------------------------------------------------------- /app/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/fedex/libs/android_sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | -keepclassmembers class com.phlox.tvwebbrowser.model.** { 16 | public *; 17 | } 18 | -keepclassmembers class com.brave.adblock.AdBlockClient { 19 | public *; 20 | private *; 21 | } 22 | -keepclassmembers class com.phlox.tvwebbrowser.webengine.webview.AndroidJSInterface { 23 | public *; 24 | private *; 25 | } 26 | 27 | 28 | #-keepclasseswithmembers class com.phlox.tvwebbrowser.model.** { 29 | # ; 30 | #} -------------------------------------------------------------------------------- /app/src/main/assets/extensions/generic/background.js: -------------------------------------------------------------------------------- 1 | console.log("TV Bro background extension loaded"); 2 | 3 | let requests = new Map(); 4 | let tvBroPort = browser.runtime.connectNative("tvbro_bg"); 5 | 6 | tvBroPort.onMessage.addListener(response => { 7 | //console.log("Received: " + JSON.stringify(response)); 8 | if (response.action === "onResolveRequest") { 9 | let id = response.data.requestId; 10 | let block = response.data.block; 11 | //console.log("Requests contents: " + JSON.stringify(Array.from(requests.keys()))); 12 | if (requests.has(id.toString())) { 13 | let request = requests.get(id.toString()); 14 | //console.log("Request resolved id: " + id); 15 | requests.delete(id.toString()); 16 | //console.log("Requests size: " + requests.size); 17 | request.resolverRef({ cancel: block }); 18 | } 19 | } 20 | }); 21 | //tvBroPort.postMessage("Hello from WebExtension!"); 22 | 23 | browser.webRequest.onBeforeRequest.addListener( 24 | function (details) { 25 | let id = details.requestId; 26 | let resolverRef = null; 27 | let promise = new Promise((resolve, reject) => { 28 | resolverRef = resolve; 29 | setTimeout(() => { 30 | if (requests.has(id)) { 31 | let request = requests.get(id); 32 | let time = new Date() - request.time; 33 | //console.log("Request block timeout id: " + id); 34 | requests.delete(id); 35 | resolve({ cancel: false }); 36 | } else { 37 | //console.log("Request processed by blocking rules id: " + id); 38 | } 39 | }, 1500); 40 | }); 41 | requests.set(id, { 42 | details: details, 43 | time: new Date(), 44 | promise: promise, 45 | resolverRef: resolverRef 46 | }); 47 | 48 | //console.log('onBeforeRequest url: ' + details.url); 49 | tvBroPort.postMessage({ action: "onBeforeRequest", details: details }); 50 | return promise; 51 | }, 52 | { urls: [""] }, 53 | ["blocking"] 54 | ); -------------------------------------------------------------------------------- /app/src/main/assets/extensions/generic/generic.js: -------------------------------------------------------------------------------- 1 | console.log("TV Bro generic content extension loaded"); 2 | -------------------------------------------------------------------------------- /app/src/main/assets/extensions/generic/home.js: -------------------------------------------------------------------------------- 1 | console.log("TV Bro home content extension loaded"); 2 | 3 | const homeExtPort = browser.runtime.connectNative("tvbro"); 4 | function postMessageToHomePagePort(action, data) { 5 | //console.log("Sending message to native app: " + action); 6 | homeExtPort.postMessage({ action: action, data: data }); 7 | } 8 | 9 | let TVBro = { 10 | startVoiceSearch: function () { 11 | postMessageToHomePagePort("startVoiceSearch"); 12 | }, 13 | setSearchEngine: function (engine, customSearchEngineURL) { 14 | postMessageToHomePagePort("setSearchEngine", { engine: engine, customSearchEngineURL: customSearchEngineURL }); 15 | }, 16 | onEditBookmark: function (bookmark) { 17 | postMessageToHomePagePort("onEditBookmark", bookmark); 18 | }, 19 | onHomePageLoaded: function () { 20 | postMessageToHomePagePort("onHomePageLoaded"); 21 | }, 22 | requestFavicon: function (url) { 23 | postMessageToHomePagePort("requestFavicon", url); 24 | }, 25 | markBookmarkRecommendationAsUseful: function (bookmarkIndex) { 26 | postMessageToHomePagePort("markBookmarkRecommendationAsUseful", bookmarkIndex); 27 | } 28 | } 29 | window.wrappedJSObject.TVBro = cloneInto( 30 | TVBro, 31 | window, 32 | { cloneFunctions: true }); 33 | 34 | homeExtPort.onMessage.addListener(message => { 35 | switch (message.action) { 36 | case "favicon": { 37 | let favicon = message.data; 38 | if (window.wrappedJSObject.onFaviconLoaded) { 39 | window.wrappedJSObject.onFaviconLoaded(favicon.url, favicon.data); 40 | } 41 | } 42 | } 43 | }); -------------------------------------------------------------------------------- /app/src/main/assets/extensions/generic/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "tvbro-generic", 4 | "version": "1.0", 5 | "description": "TV Bro Generic Addon", 6 | "browser_specific_settings": { 7 | "gecko": { 8 | "id": "tvbro@mock.com" 9 | } 10 | }, 11 | "content_scripts": [ 12 | { 13 | "matches": [ 14 | "*://*/*" 15 | ], 16 | "js": [ 17 | "generic.js" 18 | ], 19 | "run_at": "document_start" 20 | }, 21 | { 22 | "matches": [ 23 | "https://tvbro.phlox.dev/appcontent/home/*", 24 | "*://*/appcontent/home/*" 25 | ], 26 | "js": [ 27 | "home.js" 28 | ], 29 | "run_at": "document_start" 30 | } 31 | ], 32 | "background": { 33 | "scripts": [ 34 | "background.js" 35 | ], 36 | "persistent": false 37 | }, 38 | "permissions": [ 39 | "nativeMessaging", 40 | "nativeMessagingFromContent", 41 | "geckoViewAddons", 42 | "tabs", 43 | "webRequest", 44 | "webRequestBlocking", 45 | "" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/assets/generic_injects.js: -------------------------------------------------------------------------------- 1 | if (!window.tvBroClicksListener) { 2 | window.tvBroClicksListener = function(e) { 3 | if (e.target.tagName.toUpperCase() == "A" && e.target.attributes.href.value.toLowerCase().startsWith("blob:")) { 4 | var fileName = e.target.download; 5 | var url = e.target.attributes.href.value; 6 | var xhr=new XMLHttpRequest(); 7 | xhr.open('GET', e.target.attributes.href.value, true); 8 | xhr.responseType = 'blob'; 9 | xhr.onload = function(e) { 10 | if (this.status == 200) { 11 | var blob = this.response; 12 | var reader = new FileReader(); 13 | reader.readAsDataURL(blob); 14 | reader.onloadend = function() { 15 | base64data = reader.result; 16 | TVBro.takeBlobDownloadData(base64data, fileName, url, blob.type); 17 | } 18 | } 19 | }; 20 | xhr.send(); 21 | e.stopPropagation(); 22 | e.preventDefault(); 23 | } 24 | }; 25 | document.addEventListener("click", window.tvBroClicksListener); 26 | } 27 | 28 | Object.defineProperty(HTMLMediaElement.prototype, 'playing', { 29 | get: function(){ 30 | return !!(this.currentTime > 0 && !this.paused && !this.ended && this.readyState > 2); 31 | } 32 | }) 33 | 34 | window.tvBroTogglePlayback = function() { 35 | var video = document.querySelector('video'); 36 | var audio = document.querySelector('audio'); 37 | if (video) { 38 | if (video.playing) { 39 | video.pause(); 40 | } else { 41 | video.play(); 42 | } 43 | } else if (audio) { 44 | if (audio.playing) { 45 | audio.pause(); 46 | } else { 47 | audio.play(); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/assets/pages/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Boom! 5 | 6 | 7 | 20 | 21 | 22 |
23 |

Boom!

24 |

Something bad happened...

25 |

$ERROR

26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/assets/pages/ic_warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 31 | 51 | 55 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/assets/pages/warning-certificate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Security warning 5 | 6 | 60 | 61 | 62 |
63 |
64 | 65 | 66 |

67 |

68 |

69 |


70 | 71 |
72 | 73 | 74 |
75 |
76 |
77 | 99 | 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/IncognitoModeMainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity 2 | 3 | import android.app.AlertDialog 4 | import android.os.Bundle 5 | import android.util.Log 6 | import com.phlox.tvwebbrowser.R 7 | import com.phlox.tvwebbrowser.TVBro 8 | import com.phlox.tvwebbrowser.activity.main.MainActivity 9 | 10 | //Same as MainActivity but runs in separate process 11 | //and store all WebView data separately 12 | class IncognitoModeMainActivity: MainActivity() { 13 | companion object { 14 | private val TAG = MainActivity::class.java.simpleName 15 | } 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | Log.d(TAG, "onCreate") 19 | super.onCreate(savedInstanceState) 20 | if (!config.incognitoModeHintSuppress) { 21 | showIncognitoModeHintDialog() 22 | } 23 | } 24 | 25 | override fun onDestroy() { 26 | if (isFinishing) { 27 | TVBro.instance.needToExitProcessAfterMainActivityFinish = true 28 | } 29 | super.onDestroy() 30 | } 31 | 32 | private fun showIncognitoModeHintDialog() { 33 | AlertDialog.Builder(this) 34 | .setTitle(R.string.incognito_mode) 35 | .setIcon(R.drawable.ic_incognito) 36 | .setMessage(R.string.incognito_mode_hint) 37 | .setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() } 38 | .setNeutralButton(R.string.don_t_show_again) { dialog, _ -> 39 | config.incognitoModeHintSuppress = true 40 | dialog.dismiss() 41 | } 42 | .show() 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/downloads/ActiveDownloadsModel.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.downloads 2 | 3 | import android.content.ContentValues 4 | import android.net.Uri 5 | import android.os.Build 6 | import android.os.ParcelFileDescriptor 7 | import android.provider.MediaStore 8 | import android.util.Log 9 | import com.phlox.tvwebbrowser.TVBro 10 | import com.phlox.tvwebbrowser.model.Download 11 | import com.phlox.tvwebbrowser.service.downloads.DownloadTask 12 | import com.phlox.tvwebbrowser.service.downloads.FileDownloadTask 13 | import com.phlox.tvwebbrowser.singleton.AppDatabase 14 | import com.phlox.tvwebbrowser.utils.observable.ObservableList 15 | import com.phlox.tvwebbrowser.utils.activemodel.ActiveModel 16 | import java.io.File 17 | 18 | class ActiveDownloadsModel: ActiveModel() { 19 | val activeDownloads = ObservableList() 20 | private val listeners = java.util.ArrayList() 21 | 22 | interface Listener { 23 | fun onDownloadUpdated(downloadInfo: Download) 24 | fun onDownloadError(downloadInfo: Download, responseCode: Int, responseMessage: String) 25 | fun onAllDownloadsComplete() 26 | } 27 | 28 | suspend fun deleteItem(download: Download) { 29 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 30 | val contentResolver = TVBro.instance.contentResolver 31 | val rowsDeleted = contentResolver.delete(Uri.parse(download.filepath), null) 32 | if (rowsDeleted < 1) { 33 | Log.e(FileDownloadTask.TAG, "Failed to delete file from MediaStore") 34 | } 35 | } else { 36 | File(download.filepath).delete() 37 | } 38 | AppDatabase.db.downloadDao().delete(download) 39 | } 40 | 41 | fun registerListener(listener: Listener) { 42 | listeners.add(listener) 43 | } 44 | 45 | fun unregisterListener(listener: Listener) { 46 | listeners.remove(listener) 47 | } 48 | 49 | fun cancelDownload(download: Download) { 50 | for (i in activeDownloads.indices) { 51 | val task = activeDownloads[i] 52 | if (task.downloadInfo.id == download.id) { 53 | task.downloadInfo.cancelled = true 54 | break 55 | } 56 | } 57 | } 58 | 59 | fun notifyListenersAboutError(task: DownloadTask, responseCode: Int, responseMessage: String) { 60 | for (i in listeners.indices) { 61 | listeners[i].onDownloadError(task.downloadInfo, responseCode, responseMessage) 62 | } 63 | } 64 | 65 | fun notifyListenersAboutDownloadProgress(task: DownloadTask) { 66 | for (i in listeners.indices) { 67 | listeners[i].onDownloadUpdated(task.downloadInfo) 68 | } 69 | } 70 | 71 | fun onDownloadEnded(task: DownloadTask) { 72 | activeDownloads.remove(task) 73 | if (activeDownloads.isEmpty()) { 74 | for (i in listeners.indices) { 75 | listeners[i].onAllDownloadsComplete() 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/downloads/DownloadListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.downloads 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import android.widget.BaseAdapter 6 | 7 | import com.phlox.tvwebbrowser.model.Download 8 | import com.phlox.tvwebbrowser.utils.Utils 9 | 10 | import java.util.ArrayList 11 | 12 | import de.halfbit.pinnedsection.PinnedSectionListView 13 | 14 | /** 15 | * Created by PDT on 24.01.2017. 16 | */ 17 | 18 | class DownloadListAdapter(private val downloadsActivity: DownloadsActivity) : BaseAdapter(), PinnedSectionListView.PinnedSectionListAdapter { 19 | private val downloads = ArrayList() 20 | private var lastHeaderDate: Long = -1 21 | var realCount: Long = 0 22 | private set 23 | 24 | fun addItems(items: List) { 25 | if (items.isEmpty()) { 26 | return 27 | } 28 | for (download in items) { 29 | if (!Utils.isSameDate(download.time, lastHeaderDate)) { 30 | lastHeaderDate = download.time 31 | this.downloads.add(Download.createDateHeaderInfo(download.time)) 32 | } 33 | this.downloads.add(download) 34 | realCount++ 35 | } 36 | notifyDataSetChanged() 37 | } 38 | 39 | override fun getCount(): Int { 40 | return downloads.size 41 | } 42 | 43 | override fun getItem(i: Int): Any { 44 | return downloads[i] 45 | } 46 | 47 | override fun getItemId(i: Int): Long { 48 | return i.toLong() 49 | } 50 | 51 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 52 | val hiv = if (convertView != null) { 53 | convertView as DownloadListItemView 54 | } else { 55 | DownloadListItemView(downloadsActivity, getItemViewType(position)) 56 | } 57 | hiv.download = downloads[position] 58 | return hiv 59 | } 60 | 61 | override fun getViewTypeCount(): Int { 62 | return 2 63 | } 64 | 65 | override fun getItemViewType(position: Int): Int { 66 | return if (downloads[position].isDateHeader) VIEW_TYPE_HEADER else VIEW_TYPE_DOWNLOAD_ITEM 67 | } 68 | 69 | override fun isItemViewTypePinned(viewType: Int): Boolean { 70 | return viewType == VIEW_TYPE_HEADER 71 | } 72 | 73 | fun remove(download: Download) { 74 | downloads.remove(download) 75 | notifyDataSetChanged() 76 | } 77 | 78 | companion object { 79 | val VIEW_TYPE_DOWNLOAD_ITEM = 0 80 | val VIEW_TYPE_HEADER = 1 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/downloads/DownloadsHistoryModel.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.downloads 2 | 3 | import com.phlox.tvwebbrowser.model.Download 4 | import com.phlox.tvwebbrowser.singleton.AppDatabase 5 | import com.phlox.tvwebbrowser.utils.observable.ObservableValue 6 | import com.phlox.tvwebbrowser.utils.activemodel.ActiveModel 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | 10 | class DownloadsHistoryModel: ActiveModel() { 11 | val allItems = ArrayList() 12 | val lastLoadedItems = ObservableValue>(ArrayList()) 13 | private var loading = false 14 | 15 | fun loadNextItems() = modelScope.launch(Dispatchers.Main) { 16 | if (loading) { 17 | return@launch 18 | } 19 | loading = true 20 | 21 | val newItems = AppDatabase.db.downloadDao().allByLimitOffset(allItems.size.toLong()) 22 | lastLoadedItems.value = newItems 23 | allItems.addAll(newItems) 24 | 25 | loading = false 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/history/HistoryAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.history 2 | 3 | import android.util.MutableBoolean 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.BaseAdapter 7 | 8 | import com.phlox.tvwebbrowser.model.HistoryItem 9 | import com.phlox.tvwebbrowser.utils.Utils 10 | 11 | import java.util.ArrayList 12 | 13 | import de.halfbit.pinnedsection.PinnedSectionListView 14 | 15 | /** 16 | * Created by fedex on 29.12.16. 17 | */ 18 | 19 | class HistoryAdapter : BaseAdapter(), PinnedSectionListView.PinnedSectionListAdapter { 20 | val items = ArrayList() 21 | private var lastHeaderDate: Long = -1 22 | var realCount: Long = 0 23 | private set 24 | var isMultiselectMode = false 25 | set(multiselectMode) { 26 | field = multiselectMode 27 | if (!multiselectMode) { 28 | for (hi in items) { 29 | hi.selected = false 30 | } 31 | } 32 | notifyDataSetChanged() 33 | } 34 | private val _tmpSelected = ArrayList() 35 | 36 | val selectedItems: List 37 | get() { 38 | _tmpSelected.clear() 39 | for (hi in items) { 40 | if (hi.selected) { 41 | _tmpSelected.add(hi) 42 | } 43 | } 44 | return _tmpSelected 45 | } 46 | 47 | fun addItems(items: List) { 48 | if (items.isEmpty()) { 49 | return 50 | } 51 | for (hi in items) { 52 | if (!Utils.isSameDate(hi.time, lastHeaderDate)) { 53 | lastHeaderDate = hi.time 54 | this.items.add(HistoryItem.createDateHeaderInfo(hi.time)) 55 | } 56 | this.items.add(hi) 57 | realCount++ 58 | } 59 | notifyDataSetChanged() 60 | } 61 | 62 | override fun getCount(): Int { 63 | return items.size 64 | } 65 | 66 | override fun getItem(position: Int): Any { 67 | return items[position] 68 | } 69 | 70 | override fun getItemId(position: Int): Long { 71 | return position.toLong() 72 | } 73 | 74 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 75 | val hiv: HistoryItemView 76 | if (convertView != null) { 77 | hiv = convertView as HistoryItemView 78 | } else { 79 | hiv = HistoryItemView(parent.context, getItemViewType(position)) 80 | } 81 | hiv.setHistoryItem(items[position], isMultiselectMode) 82 | return hiv 83 | } 84 | 85 | override fun getViewTypeCount(): Int { 86 | return 2 87 | } 88 | 89 | override fun getItemViewType(position: Int): Int { 90 | return if (items[position].isDateHeader) VIEW_TYPE_HEADER else VIEW_TYPE_HISTORY_ITEM 91 | } 92 | 93 | override fun isItemViewTypePinned(viewType: Int): Boolean { 94 | return viewType == VIEW_TYPE_HEADER 95 | } 96 | 97 | fun erase() { 98 | items.clear() 99 | notifyDataSetChanged() 100 | } 101 | 102 | fun remove(historyItem: HistoryItem) { 103 | items.remove(historyItem) 104 | notifyDataSetChanged() 105 | } 106 | 107 | fun remove(selectedItems: List) { 108 | items.removeAll(selectedItems) 109 | notifyDataSetChanged() 110 | } 111 | 112 | companion object { 113 | val VIEW_TYPE_HISTORY_ITEM = 0 114 | val VIEW_TYPE_HEADER = 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/history/HistoryItemView.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.history 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.widget.CheckBox 6 | import android.widget.FrameLayout 7 | import android.widget.TextView 8 | 9 | import com.phlox.tvwebbrowser.R 10 | import com.phlox.tvwebbrowser.model.HistoryItem 11 | 12 | import java.text.DateFormat 13 | import java.text.SimpleDateFormat 14 | import java.util.Date 15 | 16 | /** 17 | * Created by fedex on 29.12.16. 18 | */ 19 | 20 | class HistoryItemView(context: Context, private val viewType: Int) : FrameLayout(context) { 21 | private var tvDate: TextView? = null 22 | private var tvTitle: TextView? = null 23 | private var tvURL: TextView? = null 24 | private var tvTime: TextView? = null 25 | private var cbSelection: CheckBox? = null 26 | var historyItem: HistoryItem? = null 27 | 28 | init { 29 | LayoutInflater.from(context).inflate( 30 | if (viewType == HistoryAdapter.VIEW_TYPE_HEADER) 31 | R.layout.view_history_header_item 32 | else 33 | R.layout.view_history_item, this) 34 | when (viewType) { 35 | HistoryAdapter.VIEW_TYPE_HEADER -> tvDate = findViewById(R.id.tvDate) 36 | HistoryAdapter.VIEW_TYPE_HISTORY_ITEM -> { 37 | tvTitle = findViewById(R.id.tvTitle) 38 | tvURL = findViewById(R.id.tvURL) 39 | tvTime = findViewById(R.id.tvTime) 40 | cbSelection = findViewById(R.id.cbSelection) 41 | } 42 | } 43 | } 44 | 45 | fun setHistoryItem(historyItem: HistoryItem, multiselectMode: Boolean) { 46 | this.historyItem = historyItem 47 | when (viewType) { 48 | HistoryAdapter.VIEW_TYPE_HEADER -> { 49 | val df = SimpleDateFormat.getDateInstance() 50 | tvDate!!.text = df.format(Date(historyItem.time)) 51 | } 52 | HistoryAdapter.VIEW_TYPE_HISTORY_ITEM -> { 53 | tvTitle!!.text = historyItem.title 54 | tvURL!!.text = historyItem.url 55 | val sdf = SimpleDateFormat("HH:mm") 56 | tvTime!!.text = sdf.format(Date(historyItem.time)) 57 | cbSelection!!.visibility = if (multiselectMode) VISIBLE else GONE 58 | cbSelection!!.isChecked = historyItem.selected 59 | } 60 | } 61 | } 62 | 63 | fun setSelection(selected: Boolean) { 64 | if (viewType == HistoryAdapter.VIEW_TYPE_HEADER) return 65 | cbSelection!!.isChecked = selected 66 | historyItem?.selected = selected 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/history/HistoryModel.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.history 2 | 3 | import com.phlox.tvwebbrowser.model.HistoryItem 4 | import com.phlox.tvwebbrowser.singleton.AppDatabase 5 | import com.phlox.tvwebbrowser.utils.observable.ObservableValue 6 | import com.phlox.tvwebbrowser.utils.activemodel.ActiveModel 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | 10 | class HistoryModel: ActiveModel() { 11 | val lastLoadedItems = ObservableValue>(ArrayList()) 12 | private var loading = false 13 | var searchQuery = "" 14 | 15 | 16 | fun loadItems(eraseOldResults: Boolean, offset: Long = 0) = modelScope.launch(Dispatchers.Main) { 17 | if (loading) { 18 | return@launch 19 | } 20 | loading = true 21 | 22 | lastLoadedItems.value = if ("" == searchQuery) { 23 | AppDatabase.db.historyDao().allByLimitOffset(offset) 24 | } else { 25 | AppDatabase.db.historyDao().search(searchQuery, searchQuery) 26 | } 27 | loading = false 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/main/SettingsModel.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.main 2 | 3 | import com.phlox.tvwebbrowser.Config 4 | import com.phlox.tvwebbrowser.TVBro 5 | import com.phlox.tvwebbrowser.utils.activemodel.ActiveModel 6 | import com.phlox.tvwebbrowser.utils.observable.ObservableValue 7 | 8 | class SettingsModel : ActiveModel() { 9 | companion object { 10 | val TAG = SettingsModel::class.java.simpleName 11 | const val TV_BRO_UA_PREFIX = "TV Bro/1.0 " 12 | } 13 | 14 | val config = TVBro.config 15 | 16 | //Home page settings 17 | var homePage by config::homePage 18 | var homePageMode by config::homePageMode 19 | var homePageLinksMode by config::homePageLinksMode 20 | //User agent strings configuration 21 | val userAgentStringTitles = arrayOf("Default (recommended)", "Chrome (Desktop)", "Chrome (Mobile)", "Chrome (Tablet)", "Firefox (Desktop)", "Firefox (Tablet)", "Edge (Desktop)", "Safari (Desktop)", "Safari (iPad)", "Apple TV", "Custom") 22 | val uaStrings = listOf("", 23 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36", 24 | "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Mobile Safari/537.36", 25 | "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Mobile Safari/537.36", 26 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0", 27 | "Mozilla/5.0 (Android 10; Tablet; rv:68.0) Gecko/68.0 Firefox/68.0", 28 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 Edg/84.0.522.44", 29 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15", 30 | "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1", 31 | "AppleTV6,2/11.1", 32 | "") 33 | 34 | var keepScreenOn = object : ObservableValue(config.keepScreenOn) { 35 | override var value: Boolean = config.keepScreenOn 36 | set(value) { 37 | config.keepScreenOn = value 38 | field = value 39 | notifyObservers() 40 | } 41 | get() = config.keepScreenOn 42 | }//makeObservable(config::keepScreenOn) 43 | 44 | fun setSearchEngineURL(url: String) { 45 | config.searchEngineURL.value = url 46 | if (homePageMode == Config.HomePageMode.SEARCH_ENGINE) { 47 | updateHomeAsSearchEngine(url) 48 | } 49 | } 50 | 51 | private fun updateHomeAsSearchEngine(url: String) { 52 | val regexForUrl = """^https?://[^#?/]+""".toRegex() 53 | val homePageUrl = regexForUrl.find(url)?.value ?: Config.HOME_URL_ALIAS 54 | homePage = homePageUrl 55 | } 56 | 57 | fun setHomePageProperties(homePageMode: Config.HomePageMode, customHomePageUrl: String?, homePageLinksMode: Config.HomePageLinksMode) { 58 | this.homePageMode = homePageMode 59 | this.homePageLinksMode = homePageLinksMode 60 | when (homePageMode) { 61 | Config.HomePageMode.SEARCH_ENGINE -> { 62 | updateHomeAsSearchEngine(config.searchEngineURL.value) 63 | } 64 | Config.HomePageMode.HOME_PAGE, Config.HomePageMode.BLANK -> { 65 | homePage = Config.HOME_URL_ALIAS 66 | } 67 | Config.HomePageMode.CUSTOM -> { 68 | homePage = customHomePageUrl ?: Config.HOME_URL_ALIAS 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/main/dialogs/SearchEngineConfigDialogFactory.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.main.dialogs 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.animation.AnimationUtils 7 | import android.widget.* 8 | import androidx.appcompat.app.AlertDialog 9 | import com.phlox.tvwebbrowser.Config 10 | import com.phlox.tvwebbrowser.R 11 | import com.phlox.tvwebbrowser.activity.main.SettingsModel 12 | 13 | /** 14 | * Created by fedex on 18.01.17. 15 | */ 16 | 17 | object SearchEngineConfigDialogFactory { 18 | interface Callback { 19 | fun onDone(url: String) 20 | } 21 | 22 | fun show(context: Context, settings: SettingsModel, cancellable: Boolean, callback: Callback) { 23 | 24 | var selected = 0 25 | if ("" != settings.config.searchEngineURL.value) { 26 | selected = Config.SearchEnginesURLs.indexOf(settings.config.searchEngineURL.value) 27 | } 28 | 29 | val builder = AlertDialog.Builder(context) 30 | 31 | val view = LayoutInflater.from(context).inflate(R.layout.dialog_search_engine, null) 32 | val etUrl = view.findViewById(R.id.etUrl) as EditText 33 | val llUrl = view.findViewById(R.id.llURL) as LinearLayout 34 | 35 | val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, Config.SearchEnginesTitles) 36 | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) 37 | 38 | val spEngine = view.findViewById(R.id.spEngine) as Spinner 39 | spEngine.adapter = adapter 40 | 41 | if (selected != -1) { 42 | spEngine.setSelection(selected) 43 | etUrl.setText(Config.SearchEnginesURLs[selected]) 44 | } else { 45 | spEngine.setSelection(Config.SearchEnginesTitles.size - 1) 46 | llUrl.visibility = View.VISIBLE 47 | etUrl.setText(settings.config.searchEngineURL.value) 48 | etUrl.requestFocus() 49 | } 50 | spEngine.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { 51 | override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { 52 | if (position == Config.SearchEnginesTitles.size - 1 && llUrl.visibility == View.GONE) { 53 | llUrl.visibility = View.VISIBLE 54 | llUrl.startAnimation(AnimationUtils.loadAnimation(context, android.R.anim.fade_in)) 55 | etUrl.requestFocus() 56 | } 57 | etUrl.setText(Config.SearchEnginesURLs[position]) 58 | } 59 | 60 | override fun onNothingSelected(parent: AdapterView<*>) { 61 | 62 | } 63 | } 64 | 65 | builder.setView(view) 66 | .setCancelable(cancellable) 67 | .setTitle(R.string.engine) 68 | .setPositiveButton(R.string.save) { dialog, which -> 69 | val url = etUrl.text.toString() 70 | settings.setSearchEngineURL(url) 71 | callback.onDone(url) 72 | } 73 | .show() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/main/dialogs/ShortcutDialog.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.main.dialogs 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.view.KeyEvent 6 | import android.view.View 7 | import android.widget.Button 8 | import android.widget.TextView 9 | 10 | import com.phlox.tvwebbrowser.R 11 | import com.phlox.tvwebbrowser.singleton.shortcuts.Shortcut 12 | import com.phlox.tvwebbrowser.singleton.shortcuts.ShortcutMgr 13 | 14 | /** 15 | * Created by PDT on 06.08.2017. 16 | */ 17 | 18 | class ShortcutDialog(context: Context, private val shortcut: Shortcut) : Dialog(context) { 19 | private val tvActionTitle: TextView 20 | private val tvActionKey: TextView 21 | private val btnSetKey: Button 22 | private val btnClearKey: Button 23 | private var keyListenMode = false 24 | 25 | init { 26 | setCancelable(true) 27 | setContentView(R.layout.dialog_shortcut) 28 | setTitle(R.string.shortcut) 29 | 30 | tvActionTitle = findViewById(R.id.tvActionTitle) 31 | tvActionKey = findViewById(R.id.tvActionKey) 32 | btnSetKey = findViewById(R.id.btnSetKey) 33 | btnClearKey = findViewById(R.id.btnClearKey) 34 | 35 | tvActionTitle.setText(shortcut.titleResId) 36 | updateShortcutNameDisplay() 37 | btnSetKey.setOnClickListener { toggleKeyListenState() } 38 | 39 | btnClearKey.setOnClickListener { clearKey() } 40 | } 41 | 42 | private fun clearKey() { 43 | if (keyListenMode) { 44 | toggleKeyListenState() 45 | } 46 | shortcut.keyCode = 0 47 | ShortcutMgr.getInstance().save(shortcut) 48 | updateShortcutNameDisplay() 49 | } 50 | 51 | private fun updateShortcutNameDisplay() { 52 | tvActionKey.text = if (shortcut.keyCode == 0) 53 | context.getString(R.string.not_set) 54 | else 55 | KeyEvent.keyCodeToString(shortcut.keyCode) 56 | } 57 | 58 | private fun toggleKeyListenState() { 59 | keyListenMode = !keyListenMode 60 | btnSetKey.setText(if (keyListenMode) R.string.press_eny_key else R.string.set_key_for_action) 61 | } 62 | 63 | override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 64 | if (!keyListenMode) { 65 | return super.onKeyUp(keyCode, event) 66 | } 67 | shortcut.keyCode = if (keyCode != 0) keyCode else event.scanCode 68 | ShortcutMgr.getInstance().save(shortcut) 69 | toggleKeyListenState() 70 | updateShortcutNameDisplay() 71 | return true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/main/dialogs/favorites/FavoriteEditorDialog.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.main.dialogs.favorites 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.view.View 6 | import android.widget.Button 7 | import android.widget.EditText 8 | import android.widget.ImageButton 9 | import android.widget.TextView 10 | 11 | import com.phlox.tvwebbrowser.R 12 | import com.phlox.tvwebbrowser.model.FavoriteItem 13 | 14 | /** 15 | * Created by PDT on 13.09.2016. 16 | */ 17 | class FavoriteEditorDialog(context: Context, private val callback: Callback, private val item: FavoriteItem) : Dialog(context) { 18 | private val tvTitle: TextView 19 | private val tvUrl: TextView 20 | private val etTitle: EditText 21 | private val etUrl: EditText 22 | private val ibTitle: ImageButton 23 | private val ibUrl: ImageButton 24 | private val btnDone: Button 25 | private val btnCancel: Button 26 | 27 | interface Callback { 28 | fun onDone(item: FavoriteItem) 29 | } 30 | 31 | init { 32 | setCancelable(true) 33 | setTitle(if (item.id == 0L) R.string.new_bookmark else R.string.edit) 34 | setContentView(R.layout.dialog_new_favorite_item) 35 | tvTitle = findViewById(R.id.tvTitle) as TextView 36 | tvUrl = findViewById(R.id.tvUrl) as TextView 37 | etTitle = findViewById(R.id.etTitle) as EditText 38 | etUrl = findViewById(R.id.etUrl) as EditText 39 | ibTitle = findViewById(R.id.ibTitle) as ImageButton 40 | ibUrl = findViewById(R.id.ibUrl) as ImageButton 41 | btnDone = findViewById(R.id.btnDone) as Button 42 | btnCancel = findViewById(R.id.btnCancel) as Button 43 | 44 | ibTitle.setOnClickListener { 45 | ibTitle.visibility = View.GONE 46 | tvTitle.visibility = View.GONE 47 | etTitle.visibility = View.VISIBLE 48 | etTitle.requestFocus() 49 | } 50 | 51 | ibUrl.setOnClickListener { 52 | ibUrl.visibility = View.GONE 53 | tvUrl.visibility = View.GONE 54 | etUrl.visibility = View.VISIBLE 55 | etUrl.requestFocus() 56 | } 57 | 58 | btnDone.setOnClickListener { 59 | this@FavoriteEditorDialog.item.title = etTitle.text.toString() 60 | var urlStr = etUrl.text.toString() 61 | //add https:// if url not starts with any schema 62 | if (!urlStr.matches( Regex("^[A-Za-z]+://.*$"))) { 63 | urlStr = "https://$urlStr" 64 | } 65 | this@FavoriteEditorDialog.item.url = urlStr 66 | callback.onDone(this@FavoriteEditorDialog.item) 67 | dismiss() 68 | } 69 | btnCancel.setOnClickListener { dismiss() } 70 | 71 | tvTitle.text = item.title 72 | etTitle.setText(item.title) 73 | tvUrl.text = item.url 74 | etUrl.setText(item.url) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/main/dialogs/favorites/FavoriteItemView.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.main.dialogs.favorites 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.widget.FrameLayout 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.lifecycle.lifecycleScope 10 | import com.phlox.tvwebbrowser.Config 11 | import com.phlox.tvwebbrowser.R 12 | import com.phlox.tvwebbrowser.databinding.ViewFavoriteItemBinding 13 | import com.phlox.tvwebbrowser.model.FavoriteItem 14 | import com.phlox.tvwebbrowser.singleton.FaviconsPool 15 | import com.phlox.tvwebbrowser.utils.activity 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.launch 18 | 19 | /** 20 | * Created by PDT on 13.09.2016. 21 | */ 22 | class FavoriteItemView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 23 | FrameLayout(context, attrs, defStyleAttr) { 24 | private lateinit var vb: ViewFavoriteItemBinding 25 | var favorite: FavoriteItem? = null 26 | private set 27 | var listener: Listener? = null 28 | 29 | interface Listener { 30 | fun onDeleteClick(favorite: FavoriteItem) 31 | fun onEditClick(favorite: FavoriteItem) 32 | } 33 | 34 | init { 35 | init() 36 | } 37 | 38 | private fun init() { 39 | vb = ViewFavoriteItemBinding.inflate(LayoutInflater.from(context), this, true) 40 | 41 | vb.ibDelete.setOnClickListener { favorite?.let { listener?.onDeleteClick(it)} } 42 | 43 | vb.llContent.setOnClickListener { favorite?.let {listener?.onEditClick(it)} } 44 | } 45 | 46 | fun bind(favorite: FavoriteItem, editMode: Boolean) { 47 | this.favorite = favorite 48 | vb.ibDelete.visibility = if (editMode) View.VISIBLE else View.GONE 49 | vb.llContent.isClickable = editMode 50 | vb.llContent.isFocusable = editMode 51 | vb.tvTitle.text = favorite.title 52 | vb.tvUrl.text = favorite.url 53 | vb.ivIcon.setImageResource(R.drawable.ic_not_available) 54 | val url = favorite.url 55 | if (url != null && url != Config.HOME_PAGE_URL) { 56 | val scope = (activity as AppCompatActivity).lifecycleScope 57 | scope.launch(Dispatchers.Main) { 58 | val favicon = FaviconsPool.get(url) 59 | if (url != this@FavoriteItemView.favorite?.url) return@launch //url was changed while loading favicon 60 | if (!isAttachedToWindow) return@launch 61 | favicon?.let { 62 | vb.ivIcon.setImageBitmap(it) 63 | } ?: run { 64 | vb.ivIcon.setImageResource(R.drawable.ic_not_available) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/main/dialogs/favorites/FavoritesListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.main.dialogs.favorites 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import android.widget.BaseAdapter 6 | 7 | import com.phlox.tvwebbrowser.model.FavoriteItem 8 | 9 | /** 10 | * Created by PDT on 13.09.2016. 11 | */ 12 | class FavoritesListAdapter(private val favorites: List, private val itemsListener: FavoriteItemView.Listener) : BaseAdapter() { 13 | var isEditMode = false 14 | set(editMode) { 15 | field = editMode 16 | notifyDataSetChanged() 17 | } 18 | 19 | override fun getCount(): Int { 20 | return favorites.size 21 | } 22 | 23 | override fun getItem(i: Int): Any { 24 | return favorites[i] 25 | } 26 | 27 | override fun getItemId(i: Int): Long { 28 | return i.toLong() 29 | } 30 | 31 | override fun getView(i: Int, convertView: View?, viewGroup: ViewGroup): View { 32 | val view: FavoriteItemView 33 | if (convertView != null) { 34 | view = convertView as FavoriteItemView 35 | } else { 36 | view = FavoriteItemView(viewGroup.context) 37 | view.listener = itemsListener 38 | } 39 | view.bind(favorites[i], isEditMode) 40 | return view 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/main/dialogs/settings/SettingsDialog.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.main.dialogs.settings 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.content.DialogInterface 6 | import android.view.View 7 | import com.fedir.segmentedbutton.SegmentedButton 8 | import com.phlox.tvwebbrowser.R 9 | import com.phlox.tvwebbrowser.activity.main.SettingsModel 10 | import com.phlox.tvwebbrowser.widgets.SegmentedButtonTabsAdapter 11 | 12 | class SettingsDialog(context: Context, val model: SettingsModel) : Dialog(context), DialogInterface.OnDismissListener, VersionSettingsView.Callback { 13 | private var mainView: MainSettingsView? = null 14 | private var sbTabs: SegmentedButton 15 | 16 | init { 17 | setTitle(R.string.settings) 18 | setContentView(R.layout.dialog_settings) 19 | 20 | sbTabs = findViewById(R.id.sbTabs) 21 | 22 | val tabContentAdapter = object : SegmentedButtonTabsAdapter(sbTabs, findViewById(R.id.flTabsContent)) { 23 | override fun createContentViewForSegmentButtonId(id: Int): View { 24 | return when (id) { 25 | R.id.btnMainTab -> { 26 | mainView = MainSettingsView(context) 27 | mainView!! 28 | } 29 | R.id.btnShortcutsTab -> ShortcutsSettingsView(context) 30 | else -> { 31 | val view = VersionSettingsView(context) 32 | view.callback = this@SettingsDialog 33 | view 34 | } 35 | } 36 | } 37 | } 38 | 39 | setOnDismissListener(this) 40 | } 41 | 42 | override fun onDismiss(dialog: DialogInterface?) { 43 | mainView?.save() 44 | } 45 | 46 | override fun onNeedToCloseSettings() { 47 | dismiss() 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/activity/main/view/tabs/TabsDiffUtillCallback.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.activity.main.view.tabs 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.phlox.tvwebbrowser.model.WebTabState 5 | 6 | class TabsDiffUtillCallback(val oldList: List, val newList: List): DiffUtil.Callback() { 7 | override fun getOldListSize(): Int = oldList.size 8 | 9 | override fun getNewListSize(): Int = newList.size 10 | 11 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 12 | return oldList[oldItemPosition].id == newList[newItemPosition].id 13 | } 14 | 15 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = 16 | areItemsTheSame(oldItemPosition, newItemPosition) 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/Download.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model 2 | 3 | import androidx.room.* 4 | import java.io.InputStream 5 | 6 | /** 7 | * Created by PDT on 23.01.2017. 8 | */ 9 | 10 | @Entity(tableName = "downloads", indices = arrayOf(Index(value = ["time"], name = "downloads_time_idx"), 11 | Index(value = ["filename"], name = "downloads_filename_idx"))) 12 | class Download() { 13 | @PrimaryKey(autoGenerate = true) 14 | var id: Long = 0 15 | var time: Long = 0 16 | var filename: String = "" 17 | var filepath: String = "" 18 | var url: String = "" 19 | 20 | @Volatile 21 | var size: Long = 0 22 | @Volatile @ColumnInfo(name = "bytes_received") 23 | var bytesReceived: Long = 0 24 | @Ignore 25 | var operationAfterDownload = OperationAfterDownload.NOP 26 | 27 | //non-db fields 28 | @Ignore 29 | @Volatile 30 | var cancelled: Boolean = false 31 | @Ignore 32 | var isDateHeader = false//user for displaying date headers inside list view 33 | @Ignore 34 | var mimeType: String? = null 35 | @Ignore 36 | var referer: String? = null 37 | @Ignore 38 | var userAgentString: String? = null 39 | @Ignore 40 | var base64BlobData: String? = null 41 | @Ignore 42 | var stream: InputStream? = null 43 | 44 | enum class OperationAfterDownload { 45 | NOP, INSTALL 46 | } 47 | 48 | constructor(url: String, filename: String, filepath: String?, operationAfterDownload: OperationAfterDownload, 49 | mimeType: String?, referer: String?, userAgentString: String?, base64BlobData: String?, 50 | stream: InputStream?, size: Long = 0L) : this() { 51 | this.url = url 52 | this.filename = filename 53 | this.filepath = filepath ?: "" 54 | this.operationAfterDownload = operationAfterDownload 55 | this.mimeType = mimeType 56 | this.referer = referer 57 | this.userAgentString = userAgentString 58 | this.base64BlobData = base64BlobData 59 | this.stream = stream 60 | this.size = size 61 | } 62 | 63 | companion object { 64 | val BROKEN_MARK: Long = -2 65 | val CANCELLED_MARK: Long = -3 66 | 67 | fun createDateHeaderInfo(time: Long): Download { 68 | val download = Download() 69 | download.time = time 70 | download.isDateHeader = true 71 | return download 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/FavoriteItem.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | import java.util.* 8 | 9 | /** 10 | * Created by PDT on 09.09.2016. 11 | */ 12 | @Entity(tableName = "favorites", indices = arrayOf( 13 | Index(value = ["PARENT"], name = "favorites_parent_idx"), 14 | Index(value = ["HOME_PAGE_BOOKMARK"], name = "favorites_home_page_bookmark_idx") 15 | )) 16 | class FavoriteItem { 17 | @PrimaryKey(autoGenerate = true) 18 | @ColumnInfo(name = "ID") var id: Long = 0 19 | @ColumnInfo(name = "TITLE") var title: String? = null 20 | @ColumnInfo(name = "URL") var url: String? = null 21 | @ColumnInfo(name = "PARENT") var parent: Long? = 0 22 | var favicon: String? = null 23 | @ColumnInfo(name = "HOME_PAGE_BOOKMARK") var homePageBookmark: Boolean = false 24 | @ColumnInfo(name = "I_ORDER") var order: Int = 0//used currently only for home page bookmarks because they can have blank cells in the grid 25 | @ColumnInfo(name = "DEST_URL") var destUrl: String? = null//used for initial recommendations for home page bookmarks to store referral url 26 | @ColumnInfo(name = "DESCRIPTION") var description: String? = null//used for initial recommendations for home page bookmarks if they have description 27 | @ColumnInfo(name = "VALID_UNTIL") var validUntil: Date? = null//used for initial recommendations for home page bookmarks if they have DEST_URL 28 | @ColumnInfo(name = "USEFUL") var useful: Boolean = false//used for initial recommendations for home page bookmarks if they have VALID_UNTIL 29 | val isFolder: Boolean 30 | get() = url == null 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/HistoryItem.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Ignore 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | 8 | /** 9 | * Created by fedex on 28.12.16. 10 | */ 11 | 12 | @Entity(tableName = "history", indices = arrayOf(Index(value = ["time"], name = "history_time_idx"), 13 | Index(value = ["title"], name = "history_title_idx"), Index(value = ["url"], name = "history_url_idx"))) 14 | class HistoryItem { 15 | @PrimaryKey(autoGenerate = true) 16 | var id: Long = 0 17 | var time: Long = 0 18 | var title: String = "" 19 | var url: String = "" 20 | var favicon: String? = null 21 | @Deprecated("Not used anymore") 22 | var incognito: Boolean? = null 23 | 24 | @Ignore 25 | var isDateHeader = false//used for displaying date headers inside list view 26 | @Ignore 27 | var selected = false 28 | @Ignore 29 | var saved = false 30 | 31 | companion object { 32 | 33 | fun createDateHeaderInfo(time: Long): HistoryItem { 34 | val hi = HistoryItem() 35 | hi.time = time 36 | hi.isDateHeader = true 37 | return hi 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/HomePageLink.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model 2 | 3 | import com.phlox.tvwebbrowser.TVBro 4 | import org.json.JSONObject 5 | import java.text.SimpleDateFormat 6 | import java.util.* 7 | 8 | class HomePageLink( 9 | val title: String, 10 | val url: String, 11 | val favicon: String? = null, 12 | val favoriteId: Long? = null, 13 | val order: Int? = null, 14 | val dest_url: String? = null, 15 | val description: String? = null, 16 | var validUntil: Date? = null 17 | ) { 18 | fun toJsonObj(): JSONObject { 19 | val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) 20 | return JSONObject().apply { 21 | put("title", title) 22 | put("url", url) 23 | put("favicon", favicon) 24 | put("favoriteId", favoriteId) 25 | put("order", order) 26 | put("dest_url", dest_url) 27 | put("description", description) 28 | put("validUntil", dateFormat.format(validUntil?: Date())) 29 | } 30 | } 31 | 32 | companion object { 33 | fun fromHistoryItem(item: HistoryItem): HomePageLink { 34 | return HomePageLink(item.title, item.url) 35 | } 36 | 37 | fun fromBookmarkItem(item: FavoriteItem): HomePageLink { 38 | return HomePageLink(item.title?: "", item.url?: "", item.favicon, item.id, item.order, item.destUrl, item.description, item.validUntil) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/HostConfig.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | 8 | 9 | @Entity(tableName = "hosts", indices = arrayOf( 10 | Index(value = ["host_name"], name = "hosts_name_idx", unique = true) 11 | )) 12 | class HostConfig( 13 | @ColumnInfo(name = "host_name") 14 | val hostName: String 15 | ) { 16 | @PrimaryKey(autoGenerate = true) 17 | var id: Long = 0 18 | @ColumnInfo(name = "popup_block_level") 19 | var popupBlockLevel: Int? = null 20 | var favicon: String? = null 21 | 22 | companion object { 23 | const val POPUP_BLOCK_NONE = 0 24 | const val POPUP_BLOCK_DIALOGS = 1 25 | const val POPUP_BLOCK_NEW_AUTO_OPENED_TABS = 2 26 | const val POPUP_BLOCK_NEW_TABS_EVEN_BY_USER_GESTURE = 3 27 | 28 | const val DEFAULT_BLOCK_POPUPS_VALUE = POPUP_BLOCK_NEW_AUTO_OPENED_TABS 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/dao/DownloadDao.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model.dao 2 | 3 | import androidx.room.* 4 | import com.phlox.tvwebbrowser.model.Download 5 | 6 | @Dao 7 | interface DownloadDao { 8 | @Query("SELECT * FROM downloads") 9 | suspend fun getAll(): List 10 | 11 | @Insert 12 | fun insert(download: Download): Long 13 | 14 | @Update 15 | fun update(vararg downloads: Download) 16 | 17 | @Delete 18 | suspend fun delete(download: Download) 19 | 20 | @Query("SELECT * FROM downloads ORDER BY time DESC LIMIT 100 OFFSET :offset") 21 | suspend fun allByLimitOffset(offset: Long): List 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/dao/FavoritesDao.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model.dao 2 | 3 | import androidx.room.* 4 | import com.phlox.tvwebbrowser.model.FavoriteItem 5 | import com.phlox.tvwebbrowser.model.HistoryItem 6 | 7 | @Dao 8 | interface FavoritesDao { 9 | @Query("SELECT * FROM favorites WHERE parent=0 AND home_page_bookmark=:homePageBookmarks ORDER BY id DESC") 10 | suspend fun getAll(homePageBookmarks: Boolean = false): List 11 | 12 | @Query("SELECT * FROM favorites WHERE parent=0 AND home_page_bookmark=1 ORDER BY i_order ASC") 13 | suspend fun getHomePageBookmarks(): List 14 | 15 | @Insert 16 | suspend fun insert(item: FavoriteItem): Long 17 | 18 | @Update 19 | suspend fun update(item: FavoriteItem) 20 | 21 | @Delete 22 | suspend fun delete(item: FavoriteItem) 23 | 24 | @Query("DELETE FROM favorites WHERE id=:id") 25 | suspend fun delete(id: Long) 26 | 27 | @Query("SELECT * FROM favorites WHERE id=:id") 28 | suspend fun getById(id: Long): FavoriteItem? 29 | 30 | @Query("UPDATE favorites SET useful=1 WHERE id=:favoriteId") 31 | suspend fun markAsUseful(favoriteId: Long) 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/dao/HistoryDao.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import androidx.room.RoomWarnings 8 | import com.phlox.tvwebbrowser.model.HistoryItem 9 | 10 | @Dao 11 | interface HistoryDao { 12 | @Query("SELECT * FROM history") 13 | suspend fun getAll(): List 14 | 15 | @Insert 16 | suspend fun insert(item: HistoryItem): Long 17 | 18 | @Delete 19 | suspend fun delete(vararg item: HistoryItem) 20 | 21 | @Query("DELETE FROM history WHERE time < :time") 22 | suspend fun deleteWhereTimeLessThan(time: Long) 23 | 24 | @Query("SELECT COUNT(*) FROM history") 25 | suspend fun count(): Int 26 | 27 | @Query("SELECT * FROM history ORDER BY time DESC LIMIT :limit") 28 | suspend fun last(limit: Int = 1): List 29 | 30 | @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) 31 | @Query("SELECT \"\" as id, title, url, favicon, count(url) as cnt , max(time) as time FROM history GROUP BY title, url, favicon ORDER BY cnt DESC, time DESC LIMIT 8") 32 | suspend fun frequentlyUsedUrls(): List 33 | 34 | @Query("SELECT * FROM history ORDER BY time DESC LIMIT 100 OFFSET :offset") 35 | suspend fun allByLimitOffset(offset: Long): List 36 | 37 | @Query("SELECT * FROM history WHERE (title LIKE :titleQuery) OR (url LIKE :urlQuery) ORDER BY time DESC LIMIT 100") 38 | suspend fun search(titleQuery: String, urlQuery: String): List 39 | 40 | @Query("UPDATE history SET title = :title WHERE id = :id") 41 | suspend fun updateTitle(id: Long, title: String) 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/dao/HostsDao.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model.dao 2 | 3 | import androidx.room.* 4 | import com.phlox.tvwebbrowser.model.HostConfig 5 | 6 | @Dao 7 | interface HostsDao { 8 | @Query("SELECT * FROM hosts WHERE host_name = :name") 9 | fun findByHostName(name: String): HostConfig? 10 | 11 | @Insert(onConflict = OnConflictStrategy.REPLACE) 12 | suspend fun insert(item: HostConfig): Long 13 | 14 | @Update 15 | suspend fun update(item: HostConfig) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/dao/TabsDao.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model.dao 2 | 3 | import androidx.room.* 4 | import com.phlox.tvwebbrowser.model.WebTabState 5 | 6 | @Dao 7 | interface TabsDao { 8 | @Query("SELECT * FROM tabs WHERE incognito=:incognito ORDER BY position ASC") 9 | suspend fun getAll(incognito: Boolean = false): List 10 | 11 | @Insert(onConflict = OnConflictStrategy.REPLACE) 12 | suspend fun insert(item: WebTabState): Long 13 | 14 | @Update 15 | suspend fun update(item: WebTabState) 16 | 17 | @Delete 18 | suspend fun delete(item: WebTabState) 19 | 20 | @Query("DELETE FROM tabs WHERE incognito = :incognito") 21 | suspend fun deleteAll(incognito: Boolean = false) 22 | 23 | @Query("UPDATE tabs SET selected = 0 WHERE incognito = :incognito") 24 | suspend fun unselectAll(incognito: Boolean = false) 25 | 26 | @Query("UPDATE tabs SET position = :position WHERE id = :id") 27 | suspend fun updatePosition(position: Int, id: Long) 28 | 29 | @Transaction 30 | suspend fun updatePositions(tabs: List) { 31 | tabs.forEach { updatePosition(it.position, it.id) } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/model/util/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.model.util 2 | 3 | import androidx.room.TypeConverter 4 | import java.util.* 5 | 6 | class Converters { 7 | @TypeConverter 8 | fun fromTimestamp(value: Long?): Date? { 9 | return value?.let { Date(it) } 10 | } 11 | 12 | @TypeConverter 13 | fun dateToTimestamp(date: Date?): Long? { 14 | return date?.time?.toLong() 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/singleton/shortcuts/Shortcut.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.singleton.shortcuts 2 | 3 | import android.view.KeyEvent 4 | 5 | import com.phlox.tvwebbrowser.R 6 | 7 | /** 8 | * Created by PDT on 06.08.2017. 9 | */ 10 | 11 | enum class Shortcut private constructor(var titleResId: Int, var itemId: Int, var prefsKey: String, var keyCode: Int) { 12 | MENU(R.string.toggle_main_menu, 0, "shortcut_menu", KeyEvent.KEYCODE_BACK), 13 | NAVIGATE_BACK(R.string.navigate_back, 1, "shortcut_nav_back", 0), 14 | NAVIGATE_HOME(R.string.navigate_home, 2, "shortcut_nav_home", 0), 15 | REFRESH_PAGE(R.string.refresh_page, 3, "shortcut_refresh_page", 0), 16 | VOICE_SEARCH(R.string.voice_search, 4, "shortcut_voice_search", KeyEvent.KEYCODE_SEARCH); 17 | 18 | companion object { 19 | fun findForMenu(menuId: Int): Shortcut? { 20 | for (shortcut in values()) { 21 | if (shortcut.itemId == menuId) { 22 | return shortcut 23 | } 24 | } 25 | return null 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/singleton/shortcuts/ShortcutMgr.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.singleton.shortcuts 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import com.phlox.tvwebbrowser.TVBro 6 | import com.phlox.tvwebbrowser.Config 7 | 8 | import com.phlox.tvwebbrowser.activity.main.MainActivity 9 | 10 | import java.util.HashMap 11 | 12 | /** 13 | * Created by PDT on 06.08.2017. 14 | */ 15 | 16 | class ShortcutMgr private constructor() { 17 | private val shortcuts: MutableMap 18 | private val prefs: SharedPreferences 19 | 20 | init { 21 | shortcuts = HashMap() 22 | prefs = TVBro.instance.getSharedPreferences(PREFS_SHORTCUTS, Context.MODE_PRIVATE) 23 | for (shortcut in Shortcut.values()) { 24 | shortcut.keyCode = prefs.getInt(shortcut.prefsKey, shortcut.keyCode) 25 | if (shortcut.keyCode != 0) { 26 | shortcuts[shortcut.keyCode] = shortcut 27 | } 28 | } 29 | } 30 | 31 | fun save(shortcut: Shortcut) { 32 | var oldKey = 0 33 | for ((key, value) in shortcuts) { 34 | if (value == shortcut) { 35 | oldKey = key 36 | } 37 | } 38 | if (oldKey != 0) { 39 | shortcuts.remove(oldKey) 40 | } 41 | if (shortcut.keyCode != 0) { 42 | shortcuts[shortcut.keyCode] = shortcut 43 | } 44 | prefs.edit() 45 | .putInt(shortcut.prefsKey, shortcut.keyCode) 46 | .apply() 47 | } 48 | 49 | fun findForId(id: Int): Shortcut? { 50 | for ((_, value) in shortcuts) { 51 | if (value.itemId == id) { 52 | return value 53 | } 54 | } 55 | return Shortcut.findForMenu(id) 56 | } 57 | 58 | fun process(keyCode: Int, mainActivity: MainActivity): Boolean { 59 | val shortcut = shortcuts[keyCode] ?: return false 60 | when (shortcut) { 61 | Shortcut.MENU -> { 62 | mainActivity.toggleMenu() 63 | return true 64 | } 65 | Shortcut.NAVIGATE_BACK -> { 66 | mainActivity.navigateBack() 67 | return true 68 | } 69 | Shortcut.NAVIGATE_HOME -> { 70 | mainActivity.navigate(Config.HOME_URL_ALIAS) 71 | return true 72 | } 73 | Shortcut.REFRESH_PAGE -> { 74 | mainActivity.refresh() 75 | return true 76 | } 77 | Shortcut.VOICE_SEARCH -> { 78 | mainActivity.initiateVoiceSearch() 79 | return true 80 | } 81 | } 82 | } 83 | 84 | fun canProcessKeyCode(keyCode: Int): Boolean { 85 | return shortcuts[keyCode] != null 86 | } 87 | 88 | companion object { 89 | val PREFS_SHORTCUTS = "shortcuts" 90 | const val HOME_PAGE_KEY = "home_page" 91 | private var instance: ShortcutMgr? = null 92 | 93 | @Synchronized fun getInstance(): ShortcutMgr { 94 | if (instance == null) { 95 | instance = ShortcutMgr() 96 | } 97 | return instance!! 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/AndroidBug5497Workaround.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils 2 | 3 | import android.app.Activity 4 | import android.graphics.Rect 5 | import android.view.View 6 | import android.view.ViewTreeObserver 7 | import android.widget.FrameLayout 8 | 9 | /** 10 | * Created by fedex on 14.08.16. 11 | */ 12 | class AndroidBug5497Workaround private constructor(activity: Activity) { 13 | 14 | private val mChildOfContent: View 15 | private var usableHeightPrevious: Int = 0 16 | private val frameLayoutParams: FrameLayout.LayoutParams 17 | 18 | init { 19 | val content = activity.findViewById(android.R.id.content) as FrameLayout 20 | mChildOfContent = content.getChildAt(0) 21 | mChildOfContent.viewTreeObserver.addOnGlobalLayoutListener { possiblyResizeChildOfContent() } 22 | frameLayoutParams = mChildOfContent.layoutParams as FrameLayout.LayoutParams 23 | } 24 | 25 | private fun possiblyResizeChildOfContent() { 26 | val usableHeightNow = computeUsableHeight() 27 | if (usableHeightNow != usableHeightPrevious) { 28 | val usableHeightSansKeyboard = mChildOfContent.rootView.height 29 | val heightDifference = usableHeightSansKeyboard - usableHeightNow 30 | if (heightDifference > usableHeightSansKeyboard / 4) { 31 | // keyboard probably just became visible 32 | frameLayoutParams.height = usableHeightSansKeyboard - heightDifference 33 | } else { 34 | // keyboard probably just became hidden 35 | frameLayoutParams.height = usableHeightSansKeyboard 36 | } 37 | mChildOfContent.requestLayout() 38 | usableHeightPrevious = usableHeightNow 39 | } 40 | } 41 | 42 | private fun computeUsableHeight(): Int { 43 | val r = Rect() 44 | mChildOfContent.getWindowVisibleDisplayFrame(r) 45 | return r.bottom - r.top 46 | } 47 | 48 | companion object { 49 | 50 | // For more information, see https://code.google.com/p/android/issues/detail?id=5497 51 | // To use this class, simply invoke assistActivity() on an Activity that already has its content view set. 52 | 53 | fun assistActivity(activity: Activity) { 54 | AndroidBug5497Workaround(activity) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/BaseAnimationListener.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils 2 | 3 | import android.view.animation.Animation 4 | 5 | /** 6 | * Created by PDT on 22.08.2016. 7 | */ 8 | open class BaseAnimationListener : Animation.AnimationListener { 9 | override fun onAnimationStart(animation: Animation) {} 10 | 11 | override fun onAnimationEnd(animation: Animation) {} 12 | 13 | override fun onAnimationRepeat(animation: Animation) {} 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.content.res.Resources 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import java.util.* 10 | import kotlin.collections.ArrayList 11 | 12 | val View.activity: Activity? 13 | get() { 14 | var ctx = context 15 | while (true) { 16 | if (!ContextWrapper::class.java.isInstance(ctx)) { 17 | return null 18 | } 19 | if (Activity::class.java.isInstance(ctx)) { 20 | return ctx as Activity 21 | } 22 | ctx = (ctx as ContextWrapper).baseContext 23 | } 24 | } 25 | 26 | fun Calendar.sameDay(other: Calendar): Boolean { 27 | return this.get(Calendar.YEAR) == other.get(Calendar.YEAR) && 28 | this.get(Calendar.MONTH) == other.get(Calendar.MONTH) && 29 | this.get(Calendar.DAY_OF_MONTH) == other.get(Calendar.DAY_OF_MONTH) 30 | } 31 | 32 | val ViewGroup.childs: ArrayList 33 | get() { 34 | val result = ArrayList() 35 | for (i in 0 until this.childCount) { 36 | result.add(this.getChildAt(i)) 37 | } 38 | return result 39 | } 40 | 41 | fun Int.dip2px(context: Context): Float { 42 | return (this * context.resources.displayMetrics.density) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils 2 | 3 | import android.content.Context 4 | import java.io.File 5 | import java.io.IOException 6 | 7 | @Throws(IOException::class, InterruptedException::class) 8 | fun deleteDirectory(file: File): Boolean { 9 | if (file.exists()) { 10 | val deleteCommand = "rm -rf " + file.getAbsolutePath() 11 | val runtime = Runtime.getRuntime() 12 | val process = runtime.exec(deleteCommand) 13 | process.waitFor() 14 | return true 15 | } 16 | return false 17 | } 18 | 19 | fun extractAssets(ctx: Context, assetsDir: String, destDir: File) { 20 | val assetManager = ctx.assets 21 | val files = assetManager.list(assetsDir) 22 | if (files != null) { 23 | for (file in files) { 24 | val fileName = assetsDir + File.separator + file 25 | val destFile = File(destDir, file) 26 | if (file.contains(".")) { 27 | copyAssetFile(ctx, fileName, destFile) 28 | } else { 29 | destFile.mkdirs() 30 | extractAssets(ctx, fileName, destFile) 31 | } 32 | } 33 | } 34 | } 35 | 36 | fun copyAssetFile(ctx: Context, assetPath: String, destFile: File) { 37 | val assetManager = ctx.assets 38 | val `in` = assetManager.open(assetPath) 39 | val out = destFile.outputStream() 40 | `in`.copyTo(out) 41 | `in`.close() 42 | out.close() 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/LogUtils.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils 2 | 3 | object LogUtils { 4 | //send exception info to crashlytics in case crashlytics included in current build 5 | fun recordException(e: Throwable) { 6 | try { 7 | val clazz = Class.forName("com.google.firebase.crashlytics.FirebaseCrashlytics") 8 | val method = clazz.getMethod("getInstance") 9 | val crashlytics = method.invoke(null) 10 | val clazz2 = crashlytics::class.java 11 | val method2 = clazz2.getMethod("recordException", Throwable::class.java) 12 | method2.invoke(crashlytics, e) 13 | } catch (ex: ClassNotFoundException) { 14 | //that's ok - not all builds include crashlytics 15 | } catch (ex: Exception) { 16 | ex.printStackTrace() 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils 2 | 3 | import java.io.ByteArrayOutputStream 4 | import java.io.IOException 5 | import java.io.InputStream 6 | 7 | object StringUtils { 8 | @Throws(IOException::class) 9 | fun streamToString(stream: InputStream): String { 10 | val output = ByteArrayOutputStream() 11 | val buffer = ByteArray(1024) 12 | var read = stream.read(buffer) 13 | while (read > -1) { 14 | output.write(buffer, 0, read) 15 | read = stream.read(buffer) 16 | } 17 | val result = String(output.toByteArray()) 18 | output.close() 19 | return result 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/activemodel/ActiveModel.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils.activemodel 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.SupervisorJob 6 | import kotlinx.coroutines.cancel 7 | 8 | /** 9 | * This class is like ViewModel from Jetpack but better =) 10 | * It can be used not only with activities and Fragments, but also for example 11 | * with Services or other Active models (but with manual marking as Needless - 12 | * @see com.phlox.tvwebbrowser.utils.statemodel.ActiveModelsRepository#markAsNeedless()). 13 | * This also will survive not only configuration changes but also Activity switching - 14 | * If you go from one Activity to another and they both accessing the same ActiveModel 15 | * class then actually they will access the same object what can be good for performance. 16 | * 17 | * This named Active Model because I believe that making entire View data models 18 | * (a.k.a. ViewModels) is not a really good pattern. This lead to data duplication if 19 | * different views accessing the same data states and this lead to look at View as main 20 | * logic part of application. Instead I propose to make an new kind of models what will 21 | * incorporate smaller models, their states and domain actions on them. I see this 22 | * more memory/performance effective and also more natural because in this case main 23 | * logic block of application becomes domain data and actions on it. 24 | */ 25 | abstract class ActiveModel { 26 | private var _modelScope: CoroutineScope? = null 27 | val modelScope: CoroutineScope 28 | get() { 29 | return _modelScope ?: kotlin.run { 30 | val newScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) 31 | _modelScope = newScope 32 | return newScope 33 | } 34 | } 35 | 36 | final fun clear() { 37 | _modelScope?.cancel() 38 | onClear() 39 | } 40 | 41 | open fun onClear() {} 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/activemodel/ActiveModelsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils.activemodel 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.os.Bundle 6 | import androidx.annotation.MainThread 7 | import kotlin.reflect.KClass 8 | 9 | object ActiveModelsRepository { 10 | private val holdersMap = HashMap() 11 | 12 | private class StateModelHolder(val activeModel: ActiveModel) { 13 | val users = ArrayList() 14 | } 15 | 16 | private val activityLifecycleCallbacks = object: Application.ActivityLifecycleCallbacks { 17 | override fun onActivityCreated(p0: Activity, p1: Bundle?) { 18 | } 19 | 20 | override fun onActivityStarted(p0: Activity) { 21 | } 22 | 23 | override fun onActivityResumed(p0: Activity) { 24 | } 25 | 26 | override fun onActivityPaused(p0: Activity) { 27 | } 28 | 29 | override fun onActivityStopped(p0: Activity) { 30 | } 31 | 32 | override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) { 33 | } 34 | 35 | override fun onActivityDestroyed(activity: Activity) { 36 | if (activity.isFinishing) { 37 | markAsNeedlessAllModelsUsedBy(activity) 38 | } 39 | } 40 | } 41 | 42 | fun init(app: Application){ 43 | app.registerActivityLifecycleCallbacks(activityLifecycleCallbacks) 44 | } 45 | 46 | /** 47 | * Get active model from repository. If your user is not Activity then make sure that you manually marked as needless 48 | * (@see com.phlox.tvwebbrowser.utils.statemodel.ActiveModelsRepository#markAsNeedless()) 49 | * all your "active models" when they are not needed (on your component onDestroy(), clear(), finalize() or similar) 50 | */ 51 | @Suppress("UNCHECKED_CAST") 52 | @MainThread 53 | fun get(clazz: KClass, user: Any): T { 54 | val className = clazz.qualifiedName ?: throw IllegalStateException("clazz should have name!") 55 | var modelHolder: StateModelHolder? = holdersMap[className] 56 | if (modelHolder == null) { 57 | modelHolder = StateModelHolder(clazz.java.constructors.first().newInstance() as ActiveModel) 58 | holdersMap[className] = modelHolder 59 | } 60 | if (!modelHolder.users.contains(user)) { 61 | modelHolder.users.add(user) 62 | } 63 | return modelHolder.activeModel as T 64 | } 65 | 66 | @MainThread 67 | fun markAsNeedless(activeModelUsed: ActiveModel, byUser: Any) { 68 | val key = activeModelUsed::class.qualifiedName 69 | holdersMap[key]?.let { 70 | if (it.users.remove(byUser)) { 71 | if (it.users.isEmpty()) { 72 | holdersMap.remove(key) 73 | it.activeModel.clear() 74 | } 75 | } 76 | } 77 | } 78 | 79 | @MainThread 80 | fun markAsNeedlessAllModelsUsedBy(user: Any) { 81 | val iterator = holdersMap.iterator() 82 | while (iterator.hasNext()) { 83 | val kv = iterator.next() 84 | if (kv.value.users.remove(user)) { 85 | if (kv.value.users.isEmpty()) { 86 | iterator.remove() 87 | kv.value.activeModel.clear() 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/utils/observable/ObservableList.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.utils.observable 2 | 3 | import android.os.Build.VERSION_CODES 4 | import androidx.annotation.RequiresApi 5 | import java.util.* 6 | import java.util.function.Predicate 7 | import java.util.function.UnaryOperator 8 | import kotlin.collections.ArrayList 9 | 10 | typealias ListChangeObserver = (ObservableList) -> Unit 11 | 12 | class ObservableList(): ArrayList(), Subscribable> { 13 | override val observers = ArrayList>() 14 | 15 | private fun notifyChanged() { 16 | notifyObservers() 17 | } 18 | 19 | override fun notifyObservers() { 20 | for (observer in observers) { 21 | observer(this) 22 | } 23 | } 24 | 25 | override fun notifyObserver(observer: ListChangeObserver) { 26 | observer(this) 27 | } 28 | 29 | override fun add(element: T): Boolean { 30 | val result = super.add(element) 31 | notifyChanged() 32 | return result 33 | } 34 | 35 | override fun add(index: Int, element: T) { 36 | super.add(index, element) 37 | notifyChanged() 38 | } 39 | 40 | override fun addAll(elements: Collection): Boolean { 41 | val result = super.addAll(elements) 42 | notifyChanged() 43 | return result 44 | } 45 | 46 | override fun addAll(index: Int, elements: Collection): Boolean { 47 | val result = super.addAll(index, elements) 48 | notifyChanged() 49 | return result 50 | } 51 | 52 | override fun clear() { 53 | super.clear() 54 | notifyChanged() 55 | } 56 | 57 | override fun remove(element: T): Boolean { 58 | val result = super.remove(element) 59 | notifyChanged() 60 | return result 61 | } 62 | 63 | override fun removeAll(elements: Collection): Boolean { 64 | val result = super.removeAll(elements) 65 | notifyChanged() 66 | return result 67 | } 68 | 69 | override fun retainAll(elements: Collection): Boolean { 70 | val result = super.retainAll(elements) 71 | notifyChanged() 72 | return result 73 | } 74 | 75 | @RequiresApi(VERSION_CODES.N) 76 | override fun removeIf(filter: Predicate): Boolean { 77 | val result = super.removeIf(filter) 78 | notifyChanged() 79 | return result 80 | } 81 | 82 | override fun removeAt(index: Int): T { 83 | val result = super.removeAt(index) 84 | notifyChanged() 85 | return result 86 | } 87 | 88 | override fun set(index: Int, element: T): T { 89 | val result = super.set(index, element) 90 | notifyChanged() 91 | return result 92 | } 93 | 94 | @RequiresApi(VERSION_CODES.N) 95 | override fun replaceAll(operator: UnaryOperator) { 96 | super.replaceAll(operator) 97 | notifyChanged() 98 | } 99 | 100 | override fun removeRange(fromIndex: Int, toIndex: Int) { 101 | super.removeRange(fromIndex, toIndex) 102 | notifyChanged() 103 | } 104 | 105 | fun swap(i: Int, j: Int) { 106 | super.set(i, super.set(j, super.get(i))) 107 | notifyChanged() 108 | } 109 | 110 | fun replaceAll(elements: Collection): Boolean { 111 | super.clear() 112 | val result = super.addAll(elements) 113 | notifyChanged() 114 | return result 115 | } 116 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/WebEngine.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.os.Bundle 7 | import android.view.View 8 | import android.view.ViewGroup 9 | 10 | interface WebEngine { 11 | val url: String? 12 | var userAgentString: String? 13 | 14 | fun saveState(): Any?//Bundle or any Object convertible to string 15 | fun restoreState(savedInstanceState: Any) 16 | fun stateFromBytes(bytes: ByteArray): Any? 17 | fun loadUrl(url: String) 18 | fun canGoForward(): Boolean 19 | fun goForward() 20 | fun canZoomIn(): Boolean 21 | fun zoomIn() 22 | fun canZoomOut(): Boolean 23 | fun zoomOut() 24 | fun zoomBy(zoomBy: Float) 25 | fun evaluateJavascript(script: String) 26 | fun setNetworkAvailable(connected: Boolean) 27 | fun getView(): View? 28 | @Throws(Exception::class) 29 | fun getOrCreateView(activityContext: Context): View 30 | fun canGoBack(): Boolean 31 | fun goBack() 32 | fun reload() 33 | fun onFilePicked(resultCode: Int, data: Intent?) 34 | fun onResume() 35 | fun onPause() 36 | fun onUpdateAdblockSetting(newState: Boolean) 37 | fun hideFullscreenView() 38 | fun togglePlayback() 39 | suspend fun renderThumbnail(bitmap: Bitmap?): Bitmap? 40 | /** 41 | * At this point of time web view should be already created but not attached to window 42 | */ 43 | fun onAttachToWindow(callback: WebEngineWindowProviderCallback, parent: ViewGroup, fullscreenViewParent: ViewGroup) 44 | fun onDetachFromWindow(completely: Boolean, destroyTab: Boolean) 45 | fun trimMemory() 46 | fun onPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray): Boolean 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/WebEngineFactory.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import androidx.annotation.UiThread 6 | import com.phlox.tvwebbrowser.Config 7 | import com.phlox.tvwebbrowser.TVBro 8 | import com.phlox.tvwebbrowser.activity.main.view.CursorLayout 9 | import com.phlox.tvwebbrowser.model.WebTabState 10 | import com.phlox.tvwebbrowser.utils.AndroidBug5497Workaround 11 | import com.phlox.tvwebbrowser.webengine.gecko.GeckoWebEngine 12 | import com.phlox.tvwebbrowser.webengine.webview.WebViewWebEngine 13 | 14 | object WebEngineFactory { 15 | @UiThread 16 | suspend fun initialize(context: Context, webViewContainer: CursorLayout) { 17 | if (TVBro.config.isWebEngineGecko()) { 18 | GeckoWebEngine.initialize(context, webViewContainer) 19 | //HomePageHelper.prepareHomePageFiles() 20 | } else { 21 | AndroidBug5497Workaround.assistActivity(context as Activity) 22 | } 23 | } 24 | 25 | @Suppress("KotlinConstantConditions") 26 | fun createWebEngine(tab: WebTabState): WebEngine { 27 | return if (TVBro.config.isWebEngineGecko()) 28 | GeckoWebEngine(tab) 29 | else 30 | WebViewWebEngine(tab) 31 | } 32 | 33 | suspend fun clearCache(ctx: Context) { 34 | if (TVBro.config.isWebEngineGecko()) { 35 | GeckoWebEngine.clearCache(ctx) 36 | } else { 37 | WebViewWebEngine.clearCache(ctx) 38 | } 39 | } 40 | 41 | fun onThemeSettingUpdated(value: Config.Theme) { 42 | if (TVBro.config.isWebEngineGecko()) { 43 | GeckoWebEngine.onThemeSettingUpdated(value) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/WebEngineWindowProviderCallback.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.net.Uri 7 | import android.view.View 8 | import com.phlox.tvwebbrowser.model.Download 9 | import com.phlox.tvwebbrowser.model.HomePageLink 10 | import java.io.InputStream 11 | 12 | interface WebEngineWindowProviderCallback { 13 | fun getActivity(): Activity 14 | fun onOpenInNewTabRequested(url: String, navigateImmediately: Boolean): WebEngine? 15 | fun onDownloadRequested(url: String) 16 | fun onDownloadRequested(url: String, referer: String, originalDownloadFileName: String, userAgent: String?, mimeType: String?, 17 | operationAfterDownload: Download.OperationAfterDownload = Download.OperationAfterDownload.NOP, 18 | base64BlobData: String? = null, stream: InputStream? = null, size: Long = 0L) 19 | fun onDownloadRequested(url: String, userAgent: String?, contentDisposition: String, mimetype: String?, contentLength: Long) 20 | fun onProgressChanged(newProgress: Int) 21 | fun onReceivedTitle(title: String) 22 | fun requestPermissions(array: Array): Int 23 | fun onShowFileChooser(intent: Intent): Boolean 24 | fun onReceivedIcon(icon: Bitmap) 25 | fun shouldOverrideUrlLoading(url: String): Boolean 26 | fun onPageStarted(url: String?) 27 | fun onPageFinished(url: String?) 28 | fun onPageCertificateError(url: String?) 29 | fun isAd(url: Uri, acceptHeader: String?, baseUri: Uri): Boolean? 30 | fun isAdBlockingEnabled(): Boolean 31 | fun isDialogsBlockingEnabled(): Boolean 32 | fun onBlockedAd(uri: String) 33 | fun onBlockedDialog(newTab: Boolean) 34 | fun onCreateWindow(dialog: Boolean, userGesture: Boolean): View? 35 | fun closeWindow(internalRepresentation: Any) 36 | fun onScaleChanged(oldScale: Float, newScale: Float) 37 | fun onCopyTextToClipboardRequested(url: String) 38 | fun onShareUrlRequested(url: String) 39 | fun onOpenInExternalAppRequested(url: String) 40 | fun initiateVoiceSearch() 41 | fun onEditHomePageBookmarkSelected(index: Int) 42 | fun getHomePageLinks(): List 43 | fun onPrepareForFullscreen() 44 | fun onExitFullscreen() 45 | fun onVisited(url: String) 46 | fun suggestActionsForLink(href: String, x: Int, y: Int) 47 | fun markBookmarkRecommendationAsUseful(bookmarkOrder: Int) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/gecko/GeckoViewEx.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.gecko 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import android.util.AttributeSet 7 | import android.util.Log 8 | import com.phlox.tvwebbrowser.utils.LogUtils 9 | import org.mozilla.geckoview.GeckoDisplay 10 | import org.mozilla.geckoview.GeckoDisplay.ScreenshotBuilder 11 | import org.mozilla.geckoview.GeckoResult 12 | import org.mozilla.geckoview.GeckoSession 13 | import org.mozilla.geckoview.GeckoView 14 | import kotlin.coroutines.resume 15 | import kotlin.coroutines.suspendCoroutine 16 | 17 | 18 | open class GeckoViewEx @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : 19 | GeckoView(context, attrs) { 20 | 21 | private var geckoDisplay: GeckoDisplay? = null 22 | 23 | override fun onAttachedToWindow() { 24 | super.onAttachedToWindow() 25 | screenshotBuilderAccessHack() 26 | } 27 | 28 | override fun setSession(session: GeckoSession) { 29 | super.setSession(session) 30 | screenshotBuilderAccessHack() 31 | } 32 | 33 | @Suppress("INACCESSIBLE_TYPE") 34 | private fun screenshotBuilderAccessHack() { 35 | val display: Any = mDisplay 36 | //access mDisplay field by reflection 37 | val geckoDisplayField = display.javaClass.getDeclaredField("mDisplay") 38 | geckoDisplayField.isAccessible = true 39 | geckoDisplay = geckoDisplayField.get(display) as GeckoDisplay? 40 | } 41 | 42 | fun screenshot(): ScreenshotBuilder? { 43 | geckoDisplay?.let { 44 | return it.screenshot() 45 | } 46 | return null 47 | } 48 | 49 | suspend fun renderThumbnail(bitmap: Bitmap?): Bitmap? { 50 | val screenshotBuilder = screenshot() ?: return null 51 | var thumbnail = bitmap 52 | if (thumbnail == null) { 53 | try { 54 | thumbnail = Bitmap.createBitmap(width / 2, height / 2, Bitmap.Config.ARGB_8888) 55 | } catch (e: Throwable) { 56 | e.printStackTrace() 57 | LogUtils.recordException(e) 58 | try { 59 | thumbnail = Bitmap.createBitmap(width / 4, height / 4, Bitmap.Config.ARGB_8888) 60 | } catch (e: OutOfMemoryError) { 61 | e.printStackTrace() 62 | LogUtils.recordException(e) 63 | } 64 | } 65 | } 66 | if (thumbnail == null) { 67 | return null 68 | } 69 | thumbnail = suspendCoroutine { 70 | val screenshotResult = try { 71 | screenshotBuilder.bitmap(thumbnail).capture() 72 | } catch (e: Throwable) { 73 | e.printStackTrace() 74 | it.resume(null) 75 | return@suspendCoroutine 76 | } 77 | screenshotResult.then({ bitmap -> 78 | Log.d(GeckoWebEngine.TAG, "Screenshot captured") 79 | it.resume(thumbnail) 80 | GeckoResult.fromValue(bitmap) 81 | }, { throwable -> 82 | Log.e(GeckoWebEngine.TAG, "Screenshot failed", throwable) 83 | it.resume(null) 84 | GeckoResult.fromValue(null) 85 | }) 86 | } 87 | return thumbnail 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/gecko/HomePageHelper.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.gecko 2 | 3 | import com.phlox.tvwebbrowser.TVBro 4 | import com.phlox.tvwebbrowser.utils.deleteDirectory 5 | import com.phlox.tvwebbrowser.utils.extractAssets 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | object HomePageHelper { 10 | /*private const val HOME_PAGE_VERSION = 1 11 | var homePageFilesReady: Boolean = false 12 | private const val forceExtractHomePageFiles: Boolean = true//for debug only 13 | private const val HOME_PAGE_DIR_NAME = "home_page" 14 | val HOME_PAGE_URL = "file://${TVBro.instance.filesDir}/${HOME_PAGE_DIR_NAME}/index.html" 15 | 16 | suspend fun prepareHomePageFiles() { 17 | if (homePageFilesReady) return 18 | val ctx = TVBro.instance 19 | val config = TVBro.config 20 | val filesReady = withContext(Dispatchers.IO) { 21 | val homePageDir = ctx.filesDir.resolve(HOME_PAGE_DIR_NAME) 22 | try { 23 | if (!homePageDir.exists()) { 24 | homePageDir.mkdirs() 25 | extractAssets(ctx, "pages/home", homePageDir) 26 | } else if (config.homePageVersionExtracted != HOME_PAGE_VERSION || forceExtractHomePageFiles) { 27 | deleteDirectory(homePageDir) 28 | homePageDir.mkdirs() 29 | extractAssets(ctx, "pages/home", homePageDir) 30 | config.homePageVersionExtracted = HOME_PAGE_VERSION 31 | } 32 | } catch (e: Exception) { 33 | e.printStackTrace() 34 | return@withContext false 35 | } 36 | true 37 | } 38 | 39 | homePageFilesReady = filesReady 40 | }*/ 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/gecko/delegates/AppWebExtensionBackgroundPortDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.gecko.delegates 2 | 3 | import android.graphics.Bitmap 4 | import android.net.Uri 5 | import android.util.Base64 6 | import android.util.Log 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.lifecycle.lifecycleScope 9 | import com.phlox.tvwebbrowser.TVBro 10 | import com.phlox.tvwebbrowser.singleton.FaviconsPool 11 | import com.phlox.tvwebbrowser.utils.Utils 12 | import com.phlox.tvwebbrowser.webengine.gecko.GeckoWebEngine 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.launch 15 | import kotlinx.coroutines.runBlocking 16 | import org.json.JSONArray 17 | import org.json.JSONObject 18 | import org.mozilla.geckoview.WebExtension 19 | import java.io.ByteArrayOutputStream 20 | 21 | class AppWebExtensionBackgroundPortDelegate(val port: WebExtension.Port, val webEngine: GeckoWebEngine): WebExtension.PortDelegate { 22 | override fun onPortMessage(message: Any, port: WebExtension.Port) { 23 | //Log.d(TAG, "onPortMessage: $message") 24 | try { 25 | val msgJson = message as JSONObject 26 | when (msgJson.getString("action")) { 27 | "onBeforeRequest" -> { 28 | Log.i(TAG, "onBeforeRequest: " + msgJson.toString()) 29 | val data = msgJson.getJSONObject("details") 30 | val requestId = data.getInt("requestId") 31 | val url = data.getString("url") 32 | val originUrl = data.getString("originUrl") ?: "" 33 | val type = data.getString("type") 34 | val callback = webEngine.callback ?: return 35 | val msg = JSONObject() 36 | msg.put("action", "onResolveRequest") 37 | val block = if (callback.isAdBlockingEnabled()) { 38 | callback.isAd(Uri.parse(url), type, Uri.parse(originUrl)) ?: false 39 | } else { 40 | false 41 | } 42 | if (block) { 43 | callback.onBlockedAd(url) 44 | } 45 | msg.put("data", JSONObject().put("requestId", requestId).put("block", block)) 46 | port.postMessage(msg) 47 | } 48 | } 49 | } catch (e: Exception) { 50 | e.printStackTrace() 51 | } 52 | } 53 | 54 | override fun onDisconnect(port: WebExtension.Port) { 55 | Log.d(TAG, "onDisconnect") 56 | webEngine.appWebExtensionPortDelegate = null 57 | } 58 | 59 | companion object { 60 | val TAG: String = AppWebExtensionBackgroundPortDelegate::class.java.simpleName 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/gecko/delegates/MyContentBlockingDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.gecko.delegates 2 | 3 | import com.phlox.tvwebbrowser.webengine.gecko.GeckoWebEngine 4 | import org.mozilla.geckoview.ContentBlocking 5 | import org.mozilla.geckoview.GeckoSession 6 | 7 | class MyContentBlockingDelegate(private val webEngine: GeckoWebEngine): ContentBlocking.Delegate { 8 | override fun onContentBlocked(session: GeckoSession, event: ContentBlocking.BlockEvent) { 9 | webEngine.callback?.onBlockedAd(event.uri) 10 | } 11 | 12 | override fun onContentLoaded(session: GeckoSession, event: ContentBlocking.BlockEvent) {} 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/gecko/delegates/MyHistoryDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.gecko.delegates 2 | 3 | import android.util.Log 4 | import com.phlox.tvwebbrowser.webengine.gecko.GeckoWebEngine 5 | import org.mozilla.geckoview.GeckoResult 6 | import org.mozilla.geckoview.GeckoSession 7 | 8 | class MyHistoryDelegate(private val webEngine: GeckoWebEngine) : GeckoSession.HistoryDelegate { 9 | companion object { 10 | val TAG: String = MyHistoryDelegate::class.java.simpleName 11 | } 12 | 13 | private val visitedURLs = HashSet() 14 | 15 | override fun onVisited( session: GeckoSession, url: String, lastVisitedURL: String?, flags: Int): GeckoResult { 16 | Log.i(TAG,"Visited URL: $url") 17 | visitedURLs.add(url) 18 | if (flags and GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL != 0) { 19 | webEngine.callback?.onVisited(url) 20 | } 21 | return GeckoResult.fromValue(true) 22 | } 23 | 24 | override fun getVisited(session: GeckoSession, urls: Array): GeckoResult { 25 | val visited = BooleanArray(urls.size) 26 | for (i in urls.indices) { 27 | visited[i] = visitedURLs.contains(urls[i]) 28 | } 29 | return GeckoResult.fromValue(visited) 30 | } 31 | 32 | override fun onHistoryStateChange(session: GeckoSession, historyList: GeckoSession.HistoryDelegate.HistoryList) { 33 | Log.i(TAG, "History state updated") 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/gecko/delegates/MyMediaSessionDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.gecko.delegates 2 | 3 | import org.mozilla.geckoview.GeckoSession 4 | import org.mozilla.geckoview.MediaSession 5 | 6 | class MyMediaSessionDelegate: MediaSession.Delegate { 7 | var mediaSession: MediaSession? = null 8 | var paused = false 9 | override fun onActivated(session: GeckoSession, mediaSession: MediaSession) { 10 | super.onActivated(session, mediaSession) 11 | this.mediaSession = mediaSession 12 | this.paused = false 13 | } 14 | 15 | override fun onDeactivated(session: GeckoSession, mediaSession: MediaSession) { 16 | super.onDeactivated(session, mediaSession) 17 | this.mediaSession = null 18 | } 19 | 20 | override fun onMetadata( 21 | session: GeckoSession, 22 | mediaSession: MediaSession, 23 | meta: MediaSession.Metadata 24 | ) { 25 | super.onMetadata(session, mediaSession, meta) 26 | } 27 | 28 | override fun onFeatures(session: GeckoSession, mediaSession: MediaSession, features: Long) { 29 | super.onFeatures(session, mediaSession, features) 30 | } 31 | 32 | override fun onPlay(session: GeckoSession, mediaSession: MediaSession) { 33 | super.onPlay(session, mediaSession) 34 | paused = false 35 | } 36 | 37 | override fun onPause(session: GeckoSession, mediaSession: MediaSession) { 38 | super.onPause(session, mediaSession) 39 | paused = true 40 | } 41 | 42 | override fun onStop(session: GeckoSession, mediaSession: MediaSession) { 43 | super.onStop(session, mediaSession) 44 | } 45 | 46 | override fun onPositionState( 47 | session: GeckoSession, 48 | mediaSession: MediaSession, 49 | state: MediaSession.PositionState 50 | ) { 51 | super.onPositionState(session, mediaSession, state) 52 | } 53 | 54 | override fun onFullscreen( 55 | session: GeckoSession, 56 | mediaSession: MediaSession, 57 | enabled: Boolean, 58 | meta: MediaSession.ElementMetadata? 59 | ) { 60 | super.onFullscreen(session, mediaSession, enabled, meta) 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/gecko/delegates/MyProgressDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.gecko.delegates 2 | 3 | import com.phlox.tvwebbrowser.webengine.gecko.GeckoWebEngine 4 | import org.mozilla.geckoview.GeckoSession 5 | import org.mozilla.geckoview.GeckoSession.ProgressDelegate 6 | 7 | class MyProgressDelegate(private val geckoWebEngine: GeckoWebEngine): ProgressDelegate { 8 | var sessionState: GeckoSession.SessionState? = null 9 | 10 | override fun onPageStart(session: GeckoSession, url: String) { 11 | geckoWebEngine.callback?.onPageStarted(url) 12 | } 13 | 14 | override fun onPageStop(session: GeckoSession, success: Boolean) { 15 | geckoWebEngine.callback?.onPageFinished(geckoWebEngine.url) 16 | } 17 | 18 | override fun onProgressChange(session: GeckoSession, progress: Int) { 19 | geckoWebEngine.callback?.onProgressChanged(progress) 20 | } 21 | 22 | override fun onSecurityChange( 23 | session: GeckoSession, 24 | securityInfo: ProgressDelegate.SecurityInformation 25 | ) { 26 | super.onSecurityChange(session, securityInfo) 27 | } 28 | 29 | override fun onSessionStateChange( 30 | session: GeckoSession, 31 | sessionState: GeckoSession.SessionState 32 | ) { 33 | this.sessionState = sessionState 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/webview/HomePageHelper.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.webview 2 | 3 | import android.graphics.Bitmap 4 | import android.util.Log 5 | import android.webkit.WebResourceRequest 6 | import android.webkit.WebResourceResponse 7 | import android.webkit.WebView 8 | import com.phlox.tvwebbrowser.Config 9 | import com.phlox.tvwebbrowser.TVBro 10 | import com.phlox.tvwebbrowser.singleton.FaviconsPool 11 | import kotlinx.coroutines.runBlocking 12 | import java.io.ByteArrayOutputStream 13 | import java.net.HttpURLConnection 14 | import java.net.URL 15 | 16 | object HomePageHelper { 17 | private val TAG = HomePageHelper::class.java.simpleName 18 | 19 | fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { 20 | Log.d(TAG, "shouldInterceptRequest: " + request.url) 21 | val url = request.url.toString() 22 | //check is scheme is favicon 23 | if (request.url.scheme == "favicon") { 24 | val host = request.url.host ?: return null 25 | val favicon = runBlocking { 26 | FaviconsPool.get(host) 27 | } 28 | if (favicon != null) { 29 | Log.d(TAG, "shouldInterceptRequest: favicon found for $host") 30 | val bytes = ByteArrayOutputStream() 31 | favicon.compress(Bitmap.CompressFormat.PNG, 100, bytes) 32 | return WebResourceResponse("image/png", 33 | "utf-8", bytes.toByteArray().inputStream()) 34 | } else { 35 | return WebResourceResponse(null, null, 404, "Not Found", null, null) 36 | } 37 | } /*else if (url.startsWith(Config.HOME_PAGE_URL) && 38 | (url.endsWith(".svg") || url.endsWith(".png"))) { 39 | //load images of tvbro.phlox.dev from assets for offline mode 40 | val data = TVBro.instance.assets.open("pages/home" + request.url.path!!.replace("/appcontent/home", "")).use { it.readBytes() } 41 | var imageType = url.substring(url.lastIndexOf(".") + 1) 42 | if (imageType == "svg") { 43 | imageType = "svg+xml" 44 | } 45 | return WebResourceResponse("image/" + imageType, 46 | "utf-8", data.inputStream()) 47 | }*/ 48 | return null 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/webengine/webview/Scripts.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.webengine.webview 2 | 3 | object Scripts { 4 | val INITIAL_SCRIPT = """ 5 | window.addEventListener("touchstart", function(e) { 6 | window.TVBRO_activeElement = e.target; 7 | });""" 8 | 9 | val LONG_PRESS_SCRIPT = """ 10 | var element = window.TVBRO_activeElement; 11 | if (element != null) { 12 | if ('A' == element.tagName) { 13 | element.protocol+'//'+element.host+element.pathname+element.search+element.hash; 14 | } 15 | }""" 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/widgets/CheckableContainer.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.widgets 2 | 3 | import android.R.attr 4 | import android.content.Context 5 | import android.util.AttributeSet 6 | import android.widget.Checkable 7 | import android.widget.RelativeLayout 8 | 9 | class CheckableContainer @JvmOverloads constructor( 10 | context: Context, 11 | attrs: AttributeSet? = null 12 | ) : RelativeLayout(context, attrs), Checkable { 13 | private val checkedStateSet = intArrayOf(attr.state_checked) 14 | private var mChecked = false 15 | 16 | override fun isChecked(): Boolean { 17 | return mChecked 18 | } 19 | 20 | override fun setChecked(checked: Boolean) { 21 | mChecked = checked 22 | refreshDrawableState() 23 | } 24 | 25 | override fun toggle() { 26 | mChecked = !mChecked 27 | refreshDrawableState() 28 | } 29 | 30 | override fun onCreateDrawableState(extraSpace: Int): IntArray? { 31 | val drawableState = super.onCreateDrawableState(extraSpace + 1) 32 | if (isChecked) { 33 | mergeDrawableStates(drawableState, checkedStateSet) 34 | } 35 | return drawableState 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/widgets/CheckableImageButton.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.widgets 2 | 3 | import android.R.attr 4 | import android.content.Context 5 | import android.util.AttributeSet 6 | import android.widget.Checkable 7 | import androidx.appcompat.widget.AppCompatImageButton 8 | 9 | class CheckableImageButton @JvmOverloads constructor( 10 | context: Context, attrs: AttributeSet? = null 11 | ) : AppCompatImageButton(context, attrs), Checkable { 12 | private val checkedStateSet = intArrayOf(attr.state_checked) 13 | private var mChecked = false 14 | 15 | override fun isChecked(): Boolean { 16 | return mChecked 17 | } 18 | 19 | override fun setChecked(checked: Boolean) { 20 | mChecked = checked 21 | refreshDrawableState() 22 | } 23 | 24 | override fun toggle() { 25 | mChecked = !mChecked 26 | refreshDrawableState() 27 | } 28 | 29 | override fun onCreateDrawableState(extraSpace: Int): IntArray? { 30 | val drawableState = super.onCreateDrawableState(extraSpace + 1) 31 | if (isChecked) { 32 | mergeDrawableStates(drawableState, checkedStateSet) 33 | } 34 | return drawableState 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/widgets/NotificationView.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.widgets 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | import android.view.ViewGroup 7 | import android.view.ViewPropertyAnimator 8 | import android.widget.FrameLayout 9 | import android.widget.RelativeLayout 10 | import androidx.annotation.DrawableRes 11 | import com.phlox.tvwebbrowser.databinding.ViewNotificationBinding 12 | 13 | open class NotificationView @JvmOverloads constructor( 14 | context: Context, attrs: AttributeSet? = null 15 | ) : FrameLayout(context, attrs) { 16 | private var lastAnimator: ViewPropertyAnimator? = null 17 | private var vb: ViewNotificationBinding 18 | 19 | companion object { 20 | const val DISAPPEARING_DELAY = 3000L 21 | const val APPEARING_DURATION = 500L 22 | const val DISAPPEARING_DURATION = 500L 23 | 24 | private var lastView: NotificationView? = null 25 | 26 | fun showBottomRight(parent: RelativeLayout, @DrawableRes icon: Int, message: String): NotificationView { 27 | val view = NotificationView(parent.context) 28 | view.setIcon(icon) 29 | view.setMessage(message) 30 | val lp = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) 31 | lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT) 32 | lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM) 33 | parent.addView(view, lp) 34 | val lv = lastView 35 | if (lv != null && lv.isAttachedToWindow) { 36 | val lvp = lv.parent 37 | if (lvp != null && lvp is ViewGroup) { 38 | lvp.removeView(lv) 39 | } 40 | view.postDelayed({ 41 | view.animateDisappearing() 42 | }, DISAPPEARING_DELAY) 43 | } else { 44 | view.animateAppearing() 45 | } 46 | lastView = view 47 | return view 48 | } 49 | } 50 | 51 | init { 52 | vb = ViewNotificationBinding.inflate(LayoutInflater.from(context), this, true) 53 | } 54 | 55 | private fun animateDisappearing() { 56 | lastAnimator = animate().alpha(0f).translationY(height.toFloat()).setDuration(DISAPPEARING_DURATION).withEndAction { 57 | val parent = parent 58 | if (parent != null && parent is ViewGroup) { 59 | parent.removeView(this) 60 | } 61 | lastView = null 62 | }.also { it.start() } 63 | } 64 | 65 | fun setMessage(text: String) { 66 | vb.tvMessage.text = text 67 | } 68 | 69 | fun setIcon(@DrawableRes icon: Int) { 70 | vb.ivIcon.setImageResource(icon) 71 | } 72 | 73 | fun animateAppearing() { 74 | alpha = 0f 75 | lastAnimator = animate().alpha(1.0f).setDuration(APPEARING_DURATION).withEndAction { 76 | alpha = 1f 77 | translationY = 0f 78 | postDelayed({ 79 | animateDisappearing() 80 | }, DISAPPEARING_DELAY) 81 | }.also { it.start() } 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/phlox/tvwebbrowser/widgets/SegmentedButtonTabsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.phlox.tvwebbrowser.widgets 2 | 3 | import android.util.SparseArray 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import com.fedir.segmentedbutton.SegmentedButton 7 | 8 | abstract class SegmentedButtonTabsAdapter(val segmentedButton: SegmentedButton, val contentLayout: ViewGroup) { 9 | var currentContentView: View? = null 10 | private set 11 | private val contentViewsCache = SparseArray() 12 | var callback: Callback? = null 13 | 14 | interface Callback { 15 | fun onCheckedChanged(button: SegmentedButton, checkedButtonId: Int, byUser: Boolean) 16 | } 17 | 18 | private val segmentedButtonCheckedChangeListener = SegmentedButton.OnCheckedChangeListener { button, checkedButtonId, byUser -> 19 | showTab(checkedButtonId) 20 | callback?.onCheckedChanged(button, checkedButtonId, byUser) 21 | } 22 | 23 | init { 24 | segmentedButton.checkedChangeListener = segmentedButtonCheckedChangeListener 25 | showTab(segmentedButton.checkedId) 26 | } 27 | 28 | private fun showTab(checkedSegmentId: Int) { 29 | if (checkedSegmentId == SegmentedButton.NO_ID) return 30 | contentLayout.removeAllViews() 31 | var view = contentViewsCache.get(checkedSegmentId) 32 | if (view == null) { 33 | view = createContentViewForSegmentButtonId(checkedSegmentId) 34 | contentViewsCache.put(checkedSegmentId, view) 35 | } 36 | contentLayout.addView(view, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) 37 | currentContentView = view 38 | } 39 | 40 | abstract fun createContentViewForSegmentButtonId(id: Int): View 41 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/highlight_by_scale_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/anim/infinite_fadeinout_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/right_menu_in_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/right_menu_out_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/webtab_horizontal_text_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-hdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-mdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-night-hdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-night-hdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-night-mdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-night-mdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-night-xhdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-night-xhdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-night-xxhdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-night-xxhdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-night-xxxhdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-night-xxxhdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-xhdpi/banner.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-xhdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-xxhdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/tab_patch.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truefedex/tv-bro/2bfd76b033253d5be0210b4e1107b2a1cbaf0da0/app/src/main/res/drawable-xxxhdpi/tab_patch.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/back_icon_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_bg_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/favorite_item_view_bg_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/forward_icon_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gray_badge_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_adblock_off.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 16 | 19 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_adblock_on.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 16 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_grey_400_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_grey_900_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_forward_grey_400_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_forward_grey_900_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_add_box_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_block_popups.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_grey_900_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_grey_900_36dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_grey_400_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_grey_900_36dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_download_grey_900.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_history_grey_900_36dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_grey_900_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_incognito.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_right_grey_900_18dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_grey_900_36dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_none_grey_900_36dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mode_edit_grey_400_18dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_not_available.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh_grey_900_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_grey_900_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_border_grey_900_36dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_in_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_in_gray_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_out_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_out_gray_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_item_bg_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/settings_tab_bg_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_button_bg_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/text_link_background_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/webtab_horizontal_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/webtab_horizontal_bkg_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/zoomin_icon_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/zoomout_icon_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_downloads.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_history.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 |