├── settings.gradle ├── fastlane └── metadata │ └── android │ └── en-GB │ ├── video.txt │ ├── title.txt │ ├── short_description.txt │ ├── images │ ├── icon.png │ ├── featureGraphic.jpg │ └── phoneScreenshots │ │ ├── screen1.png │ │ ├── screen2.png │ │ ├── screen3.png │ │ └── screen4.png │ └── full_description.txt ├── local.properties.sample ├── images ├── wms1.png ├── wms2.png ├── wms3.png ├── fdroid.png └── google_play.png ├── app ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-hdpi │ │ │ │ ├── ic_action_cast.png │ │ │ │ ├── ic_action_exit.png │ │ │ │ ├── ic_action_font.png │ │ │ │ ├── ic_action_open.png │ │ │ │ ├── ic_action_save.png │ │ │ │ ├── ic_action_site.png │ │ │ │ ├── ic_action_stop.png │ │ │ │ ├── ic_action_tips.png │ │ │ │ ├── ic_format_size.png │ │ │ │ ├── ic_action_image.png │ │ │ │ ├── ic_action_share.png │ │ │ │ ├── ic_action_3rd_party.png │ │ │ │ ├── ic_action_refresh.png │ │ │ │ ├── ic_action_settings.png │ │ │ │ ├── ic_action_shortcut.png │ │ │ │ ├── ic_action_certificate.png │ │ │ │ ├── ic_action_user_agent.png │ │ │ │ └── ic_action_broken_image.png │ │ │ ├── drawable-mdpi │ │ │ │ ├── ic_action_cast.png │ │ │ │ ├── ic_action_exit.png │ │ │ │ ├── ic_action_font.png │ │ │ │ ├── ic_action_open.png │ │ │ │ ├── ic_action_save.png │ │ │ │ ├── ic_action_site.png │ │ │ │ ├── ic_action_stop.png │ │ │ │ ├── ic_action_tips.png │ │ │ │ ├── ic_format_size.png │ │ │ │ ├── ic_action_image.png │ │ │ │ ├── ic_action_share.png │ │ │ │ ├── ic_action_3rd_party.png │ │ │ │ ├── ic_action_refresh.png │ │ │ │ ├── ic_action_settings.png │ │ │ │ ├── ic_action_shortcut.png │ │ │ │ ├── ic_action_certificate.png │ │ │ │ ├── ic_action_user_agent.png │ │ │ │ └── ic_action_broken_image.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xhdpi │ │ │ │ ├── ic_action_cast.png │ │ │ │ ├── ic_action_exit.png │ │ │ │ ├── ic_action_font.png │ │ │ │ ├── ic_action_image.png │ │ │ │ ├── ic_action_open.png │ │ │ │ ├── ic_action_save.png │ │ │ │ ├── ic_action_share.png │ │ │ │ ├── ic_action_site.png │ │ │ │ ├── ic_action_stop.png │ │ │ │ ├── ic_action_tips.png │ │ │ │ ├── ic_format_size.png │ │ │ │ ├── ic_action_refresh.png │ │ │ │ ├── ic_action_settings.png │ │ │ │ ├── ic_action_shortcut.png │ │ │ │ ├── ic_action_3rd_party.png │ │ │ │ ├── ic_action_user_agent.png │ │ │ │ ├── ic_action_broken_image.png │ │ │ │ └── ic_action_certificate.png │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── ic_action_cast.png │ │ │ │ ├── ic_action_exit.png │ │ │ │ ├── ic_action_open.png │ │ │ │ ├── ic_action_save.png │ │ │ │ ├── ic_action_stop.png │ │ │ │ ├── ic_action_tips.png │ │ │ │ ├── ic_format_size.png │ │ │ │ ├── ic_action_image.png │ │ │ │ ├── ic_action_refresh.png │ │ │ │ ├── ic_action_share.png │ │ │ │ ├── ic_action_3rd_party.png │ │ │ │ ├── ic_action_settings.png │ │ │ │ ├── ic_action_shortcut.png │ │ │ │ ├── ic_action_broken_image.png │ │ │ │ ├── ic_action_certificate.png │ │ │ │ └── ic_action_user_agent.png │ │ │ ├── drawable-xxxhdpi │ │ │ │ └── ic_format_size.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── sqlmaps.xml │ │ │ │ ├── arrays.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── drawable │ │ │ │ └── loader_bg.xml │ │ │ ├── layout │ │ │ │ ├── preferences.xml │ │ │ │ ├── actionbar_favicon.xml │ │ │ │ ├── main.xml │ │ │ │ ├── dlg_save.xml │ │ │ │ ├── webapp.xml │ │ │ │ ├── row_media_url.xml │ │ │ │ ├── row_webapp.xml │ │ │ │ ├── dlg_open_url.xml │ │ │ │ ├── dlg_certificate.xml │ │ │ │ └── dlg_certificate_changed.xml │ │ │ ├── values-ja │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-de │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── values-fr │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── menu │ │ │ │ ├── main_menu.xml │ │ │ │ └── webapps_menu.xml │ │ │ ├── xml │ │ │ │ └── settings.xml │ │ │ └── values-nl │ │ │ │ └── strings.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── tobykurien │ │ │ │ └── webmediashare │ │ │ │ ├── data │ │ │ │ ├── ThirdPartyDomain.xtend │ │ │ │ ├── MediaUrl.xtend │ │ │ │ └── Webapp.xtend │ │ │ │ ├── fragment │ │ │ │ ├── PreferencesFragment.xtend │ │ │ │ ├── DlgCertificateChanged.xtend │ │ │ │ ├── DlgCertificate.xtend │ │ │ │ ├── DlgSaveWebapp.xtend │ │ │ │ ├── DlgShareMedia.xtend │ │ │ │ └── DlgOpenUrl.xtend │ │ │ │ ├── utils │ │ │ │ ├── Debug.xtend │ │ │ │ ├── Dependencies.xtend │ │ │ │ ├── Settings.xtend │ │ │ │ ├── CertificateUtils.xtend │ │ │ │ └── FaviconHandler.xtend │ │ │ │ ├── webviewclient │ │ │ │ ├── WebViewUtilsApi12.xtend │ │ │ │ ├── WebViewUtilsApi21.xtend │ │ │ │ ├── WebViewUtilsApi16.xtend │ │ │ │ ├── WebViewUtils.xtend │ │ │ │ ├── WebViewUtilsApi11.xtend │ │ │ │ ├── WebViewUtilsApi19.xtend │ │ │ │ └── WebClient.xtend │ │ │ │ ├── activity │ │ │ │ ├── Preferences.xtend │ │ │ │ ├── ShortcutActivity.xtend │ │ │ │ ├── MainActivity.xtend │ │ │ │ └── BaseWebAppActivity.xtend │ │ │ │ ├── ssl │ │ │ │ └── SslTrustManager.xtend │ │ │ │ ├── adapter │ │ │ │ ├── MediaUrlsAdapter.xtend │ │ │ │ └── WebappsAdapter.xtend │ │ │ │ └── db │ │ │ │ └── DbService.xtend │ │ └── AndroidManifest.xml │ ├── debug │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ └── androidTest │ │ └── java │ │ └── com │ │ └── tobykurien │ │ └── webmediashare │ │ └── test │ │ └── DomainTest.xtend ├── build.gradle └── app.iml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── method_count.sh ├── .gitignore ├── debug.sh ├── test └── test_sandbox.html ├── WebMediaShare.iml ├── LICENSE.txt ├── proguard.cfg ├── README.md ├── gradlew.bat ├── gradlew └── proguard-project.txt /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/video.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/title.txt: -------------------------------------------------------------------------------- 1 | WebMediaShare -------------------------------------------------------------------------------- /local.properties.sample: -------------------------------------------------------------------------------- 1 | sdk.dir=/opt/android-sdk-linux/ 2 | 3 | -------------------------------------------------------------------------------- /images/wms1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/images/wms1.png -------------------------------------------------------------------------------- /images/wms2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/images/wms2.png -------------------------------------------------------------------------------- /images/wms3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/images/wms3.png -------------------------------------------------------------------------------- /images/fdroid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/images/fdroid.png -------------------------------------------------------------------------------- /images/google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/images/google_play.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/short_description.txt: -------------------------------------------------------------------------------- 1 | Share embedded music and videos from websites to VLC, Kodi TV, etc. -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /method_count.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | unzip -p app/build/outputs/apk/app-debug.apk classes.dex | head -c 92 | tail -c 4 | hexdump -e '1/4 "%d\n"' 3 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_cast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_cast.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_exit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_font.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_open.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_site.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_stop.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_tips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_tips.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_format_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_format_size.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_cast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_cast.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_exit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_font.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_open.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_site.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_stop.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_tips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_tips.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_format_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_format_size.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/fastlane/metadata/android/en-GB/images/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_cast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_cast.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_exit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_font.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_open.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_site.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_stop.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_tips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_tips.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_format_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_format_size.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_cast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_cast.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_exit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_open.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_stop.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_tips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_tips.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_format_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_format_size.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_3rd_party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_3rd_party.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_settings.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_shortcut.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_3rd_party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_3rd_party.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_settings.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_shortcut.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_settings.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_shortcut.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_format_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxxhdpi/ic_format_size.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_certificate.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_user_agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_user_agent.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_certificate.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_user_agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_user_agent.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_3rd_party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_3rd_party.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_user_agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_user_agent.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_3rd_party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_3rd_party.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_settings.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_shortcut.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_broken_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-hdpi/ic_action_broken_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_broken_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-mdpi/ic_action_broken_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_broken_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_broken_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xhdpi/ic_action_certificate.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_broken_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_broken_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_certificate.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_user_agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/drawable-xxhdpi/ic_action_user_agent.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/images/featureGraphic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/fastlane/metadata/android/en-GB/images/featureGraphic.jpg -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #980CA6 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | .gradle 4 | build 5 | xtend-gen 6 | keystore.properties 7 | aarDependencies 8 | /libs/ 9 | /.settings 10 | *~ 11 | /local.properties 12 | .idea 13 | 14 | -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebMediaShare DEBUG 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/images/phoneScreenshots/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/fastlane/metadata/android/en-GB/images/phoneScreenshots/screen1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/images/phoneScreenshots/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/fastlane/metadata/android/en-GB/images/phoneScreenshots/screen2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/images/phoneScreenshots/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/fastlane/metadata/android/en-GB/images/phoneScreenshots/screen3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/images/phoneScreenshots/screen4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobykurien/WebMediaShare/HEAD/fastlane/metadata/android/en-GB/images/phoneScreenshots/screen4.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/loader_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8dp 4 | 16dp 5 | 72dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/data/ThirdPartyDomain.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.data 2 | 3 | import org.eclipse.xtend.lib.annotations.Accessors 4 | 5 | @Accessors class ThirdPartyDomain { 6 | long id 7 | long webappId 8 | String domain 9 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/preferences.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -e $1 ]; then 3 | FILTER="com.tobykurien.webmediashare" 4 | else 5 | FILTER="$1" 6 | fi 7 | 8 | echo Compiling app... 9 | ./gradlew --no-daemon installDebug runApp |grep ERROR 10 | 11 | adb logcat -c 12 | adb logcat -v color -e "$FILTER" "*:D" 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 09 11:11:46 SAST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 最小 5 | 6 | 通常 7 | 8 | 最大 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sehr klein 5 | Klein 6 | Normal 7 | Groß 8 | Sehr groß 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/actionbar_favicon.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/data/MediaUrl.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.data 2 | 3 | import org.eclipse.xtend.lib.annotations.Accessors 4 | import android.net.Uri 5 | 6 | @Accessors 7 | class MediaUrl { 8 | Uri uri 9 | String contentType 10 | Long contentLength 11 | 12 | override toString() { 13 | contentType + ", " + contentLength + " bytes, " + uri.toString() + "\n" 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/fragment/PreferencesFragment.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.fragment 2 | 3 | import android.preference.PreferenceFragment 4 | import android.os.Bundle 5 | import com.tobykurien.webmediashare.R 6 | 7 | class PreferencesFragment extends PreferenceFragment { 8 | 9 | override onActivityCreated(Bundle savedInstanceState) { 10 | super.onActivityCreated(savedInstanceState) 11 | addPreferencesFromResource(R.xml.settings) 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/data/Webapp.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.data 2 | 3 | import org.eclipse.xtend.lib.annotations.Accessors 4 | import org.eclipse.xtend.lib.annotations.ToString 5 | 6 | /** 7 | * Webapp POJO to store details of a webapp 8 | */ 9 | @Accessors @ToString class Webapp { 10 | long id 11 | String name 12 | String url 13 | int fontSize = -1 14 | String userAgent 15 | String certIssuedTo 16 | String certIssuedBy 17 | String certValidFrom 18 | String certValidTo 19 | String cookies 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/utils/Debug.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.utils 2 | 3 | import com.tobykurien.webmediashare.BuildConfig 4 | 5 | /** 6 | * Global debug switches for testing and debugging 7 | */ 8 | class Debug { 9 | public val static boolean ON = BuildConfig.DEBUG // global on/off switch. Turn off for production 10 | 11 | public val static boolean FAVICON = ON && false // spit out debug info for favicon handling 12 | public val static boolean COOKIE = ON && false // spit out debug info for cookie handling 13 | } -------------------------------------------------------------------------------- /app/src/main/res/values-fr/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | Le plut petit 11 | Plus petit 12 | Normal 13 | Plus grand 14 | Le plus grand 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #e800ec 5 | #a7009c 6 | #FFECB3 7 | #34ff22 8 | #212121 9 | #727272 10 | #212121 11 | #B6B6B6 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/utils/Dependencies.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.utils 2 | 3 | import android.content.Context 4 | import com.tobykurien.webmediashare.db.DbService 5 | 6 | /** 7 | * Singleton factory for commonly used dependencies like database and shared preferences. 8 | */ 9 | class Dependencies { 10 | def static DbService getDb(Context context) { 11 | return DbService.getInstance(context) 12 | } 13 | 14 | def static Settings getSettings(Context context) { 15 | return Settings.getPreferences(context, Settings) as Settings 16 | } 17 | } -------------------------------------------------------------------------------- /test/test_sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | Test sandbox 3 | 4 | Test Sandbox

5 | 6 |

7 | Simple test to check the sandbox that requires running a netcat session on the local network 8 | and a web server that attempts to set and read a cookie. Saving this page as multiple 9 | sandboxes can then allow testing if sandboxes are leaking referer and cookie information. 10 |

11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/webviewclient/WebViewUtilsApi12.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.webviewclient 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.webkit.CookieManager 6 | import android.webkit.WebView 7 | import com.tobykurien.webmediashare.data.Webapp 8 | import android.annotation.TargetApi 9 | 10 | @TargetApi(12) 11 | class WebViewUtilsApi12 extends WebViewUtilsApi11 { 12 | 13 | override setupWebView(Context context, WebView wv, Uri siteUrl, Webapp webapp, int defaultFontSize) { 14 | super.setupWebView(context, wv, siteUrl, webapp, defaultFontSize) 15 | 16 | CookieManager.setAcceptFileSchemeCookies(false); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/webviewclient/WebViewUtilsApi21.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.webviewclient 2 | 3 | import android.annotation.TargetApi 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.webkit.WebView 7 | import com.tobykurien.webmediashare.data.Webapp 8 | import android.webkit.* 9 | import android.util.Log 10 | 11 | @TargetApi(21) 12 | class WebViewUtilsApi21 extends WebViewUtilsApi19 { 13 | 14 | override setupWebView(Context context, WebView wv, Uri siteUrl, Webapp webapp, int defaultFontSize) { 15 | super.setupWebView(context, wv, siteUrl, webapp, defaultFontSize) 16 | 17 | val cookieManager = CookieManager.instance 18 | cookieManager.setAcceptThirdPartyCookies(wv, false) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/activity/Preferences.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.activity 2 | 3 | import org.xtendroid.utils.BasePreferences 4 | import com.tobykurien.webmediashare.R 5 | import com.tobykurien.webmediashare.R.xml 6 | import android.os.Bundle 7 | import android.preference.PreferenceActivity 8 | import android.support.v7.app.AppCompatActivity 9 | 10 | class Preferences extends AppCompatActivity { 11 | override protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.preferences) 14 | } 15 | 16 | override protected void onPause() { 17 | super.onPause() // tell Webview to reload with new settings 18 | BasePreferences.clearCache() 19 | BaseWebAppActivity.reload = true 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/ssl/SslTrustManager.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.ssl 2 | 3 | import com.tobykurien.webmediashare.data.Webapp 4 | import java.security.cert.CertificateException 5 | import java.security.cert.X509Certificate 6 | import javax.net.ssl.X509TrustManager 7 | 8 | class SslTrustManager implements X509TrustManager { 9 | var Webapp webapp 10 | 11 | public new(Webapp webapp) { 12 | this.webapp = webapp 13 | } 14 | 15 | override checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 16 | } 17 | 18 | override checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 19 | // TODO - verify SSL certificate against webapp's saved certificate details 20 | } 21 | 22 | override getAcceptedIssuers() { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WebMediaShare.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-GB/full_description.txt: -------------------------------------------------------------------------------- 1 | WebMediaShare is an app to browse your favourite media websites (e.g. online streaming sites, online radio stations, etc.) so that you can: 2 | 3 | - view the content without ads/popups/redirects/etc. 4 | - listen to music from a streaming site in a media player app like VLC, so that it continues playing even if the screen is off 5 | - send the media to your TV or Hifi (e.g. via the Kore app for Kodi). This works like Chromecast. 6 | - share the media URL to friends on chats or email 7 | - share the media to an app for downloading 8 | 9 | WebMediaShare is a browser with the following features: 10 | 11 | - Save your favourite media sites in-app 12 | - Add shortcuts to the home screen so that they open like regular apps 13 | - Ad blocking 14 | - Prevents popups, popunders, and redirects 15 | - intercepts media within web pages, allowing you to view and share them 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dlg_save.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/adapter/MediaUrlsAdapter.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.adapter 2 | 3 | import org.xtendroid.adapter.AndroidAdapter 4 | import java.util.List 5 | import com.tobykurien.webmediashare.data.MediaUrl 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import org.xtendroid.adapter.AndroidViewHolder 9 | import com.tobykurien.webmediashare.R 10 | 11 | @AndroidAdapter class MediaUrlsAdapter { 12 | List mediaUrls 13 | 14 | /** 15 | * ViewHolder class to save references to UI widgets in each row 16 | */ 17 | @AndroidViewHolder(R.layout.row_media_url) static class ViewHolder { 18 | } 19 | 20 | override getView(int row, View cv, ViewGroup parent) { 21 | var vh = ViewHolder.getOrCreate(context, cv, parent) 22 | var mediaUrl = getItem(row) 23 | vh.name.text = mediaUrl.uri.host + " " + mediaUrl.getContentType 24 | vh.url.text = mediaUrl.uri.path 25 | 26 | vh.view 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/webapp.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 21 | 22 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/webviewclient/WebViewUtilsApi16.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.webviewclient 2 | 3 | import com.tobykurien.webmediashare.webviewclient.WebViewUtilsApi12 4 | import android.content.Context 5 | import android.webkit.WebView 6 | import android.net.Uri 7 | import com.tobykurien.webmediashare.data.Webapp 8 | import android.annotation.TargetApi 9 | 10 | @TargetApi(16) 11 | class WebViewUtilsApi16 extends WebViewUtilsApi12 { 12 | 13 | override setupWebView(Context context, WebView wv, Uri siteUrl, Webapp webapp, int defaultFontSize) { 14 | super.setupWebView(context, wv, siteUrl, webapp, defaultFontSize) 15 | 16 | var settings = wv.getSettings(); 17 | settings.allowFileAccessFromFileURLs = false 18 | settings.allowUniversalAccessFromFileURLs = false 19 | } 20 | 21 | override setTextSize(WebView wv, int size) { 22 | wv.settings.textZoom = switch(size) { 23 | case 0: 50 24 | case 1: 75 25 | case 2: 100 26 | case 3: 125 27 | case 4: 150 28 | default: 100 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Toby Kurien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/row_media_url.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/row_webapp.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | 23 | 24 | 33 | 34 | -------------------------------------------------------------------------------- /proguard.cfg: -------------------------------------------------------------------------------- 1 | -optimizationpasses 5 2 | -dontusemixedcaseclassnames 3 | -dontskipnonpubliclibraryclasses 4 | -dontpreverify 5 | -verbose 6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 7 | 8 | -keep public class * extends android.app.Activity 9 | -keep public class * extends android.app.Application 10 | -keep public class * extends android.app.Service 11 | -keep public class * extends android.content.BroadcastReceiver 12 | -keep public class * extends android.content.ContentProvider 13 | -keep public class * extends android.app.backup.BackupAgentHelper 14 | -keep public class * extends android.preference.Preference 15 | -keep public class com.android.vending.licensing.ILicensingService 16 | 17 | -keepclasseswithmembernames class * { 18 | native ; 19 | } 20 | 21 | -keepclasseswithmembers class * { 22 | public (android.content.Context, android.util.AttributeSet); 23 | } 24 | 25 | -keepclasseswithmembers class * { 26 | public (android.content.Context, android.util.AttributeSet, int); 27 | } 28 | 29 | -keepclassmembers class * extends android.app.Activity { 30 | public void *(android.view.View); 31 | } 32 | 33 | -keepclassmembers enum * { 34 | public static **[] values(); 35 | public static ** valueOf(java.lang.String); 36 | } 37 | 38 | -keep class * implements android.os.Parcelable { 39 | public static final android.os.Parcelable$Creator *; 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/db/DbService.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.db 2 | 3 | import android.content.Context 4 | import android.database.sqlite.SQLiteDatabase 5 | import android.webkit.CookieSyncManager 6 | import com.tobykurien.webmediashare.data.Webapp 7 | import java.util.List 8 | import org.xtendroid.db.BaseDbService 9 | import android.util.Log 10 | import android.webkit.CookieManager 11 | import com.tobykurien.webmediashare.utils.Debug 12 | 13 | /** 14 | * Class to manage database queries. Uses Xtendroid's BaseDbService 15 | */ 16 | class DbService extends BaseDbService { 17 | public static val TABLE_WEBAPPS = "webapps" 18 | public static val TABLE_DOMAINS = "domain_names" 19 | 20 | protected new(Context context) { 21 | super(context, "webmediashare", 1) 22 | } 23 | 24 | def static getInstance(Context context) { 25 | return new DbService(context) 26 | } 27 | 28 | override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 29 | super.onUpgrade(db, oldVersion, newVersion) 30 | } 31 | 32 | def List getWebapps() { 33 | findAll(TABLE_WEBAPPS, "lower(name) asc", Webapp) 34 | } 35 | 36 | def void saveCookies(Webapp webapp) { 37 | if (Debug.COOKIE) Log.d("cookie", "Saving cookies for " + webapp.url) 38 | var cookiesStr = CookieManager.instance.getCookie(webapp.url) 39 | if (cookiesStr != null) { 40 | update("webapps", #{ 41 | "cookies" -> cookiesStr 42 | }, webapp.id) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/utils/Settings.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.utils 2 | 3 | import org.xtendroid.annotations.AndroidPreference 4 | 5 | /** 6 | * Class to get and set shared preferences, which are also editable from the Preference activity. 7 | * Uses Xtendroid's @AndroidPreference to manage the preferences, making the class appear as a POJO. 8 | * NOTE: Default values here must also match up with the default values in settings.xml 9 | */ 10 | @AndroidPreference class Settings { 11 | boolean block3rdParty = true 12 | //boolean blockHttp = true // deprecated 13 | String fontSize = "2" 14 | String userAgent = "" 15 | boolean fullscreen = false 16 | boolean fullscreenImmersive = false 17 | boolean hideActionbar = true 18 | boolean loadImages = true 19 | int firstLoaded = 0 20 | boolean fullHideActionbar = false 21 | boolean fullHideShortcutOnly = false 22 | boolean cookiesImported = false // cookies imported to db? 23 | 24 | long lastWebappId = -1 25 | 26 | def getIntFontSize() { 27 | try { 28 | Integer.parseInt(getFontSize()) 29 | } catch(Exception e) { 30 | 2 31 | } 32 | } 33 | 34 | def isBlockHttp() { 35 | // Deprecate old option to allow HTTP 3rd party requests 36 | return true 37 | } 38 | 39 | def boolean shouldHideActionBar(boolean isFromShortcut) { 40 | if (isFullHideActionbar && !isFullHideShortcutOnly) return true; 41 | if (isFullHideActionbar && isFullHideShortcutOnly && isFromShortcut) return true; 42 | return false; 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/webviewclient/WebViewUtils.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.webviewclient 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.os.Build 6 | import android.webkit.WebSettings.TextSize 7 | import android.webkit.WebView 8 | import com.tobykurien.webmediashare.data.Webapp 9 | 10 | abstract class WebViewUtils { 11 | def static WebViewUtils getInstance() { 12 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 13 | return new WebViewUtilsApi21(); 14 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 15 | return new WebViewUtilsApi19(); 16 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 17 | return new WebViewUtilsApi16(); 18 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { 19 | return new WebViewUtilsApi12(); 20 | } else { 21 | return new WebViewUtilsApi11(); 22 | } 23 | } 24 | 25 | def void setTextSize(WebView wv, int size) { 26 | var textSize = TextSize.NORMAL; 27 | 28 | switch (size) { 29 | case 0: textSize = TextSize.SMALLEST 30 | case 1: textSize = TextSize.SMALLER 31 | case 2: textSize = TextSize.NORMAL 32 | case 3: textSize = TextSize.LARGER 33 | case 4: textSize = TextSize.LARGEST 34 | } 35 | 36 | wv.getSettings().setTextSize(textSize); 37 | } 38 | 39 | def abstract void setupWebView(Context context, WebView wv, 40 | Uri siteUrl, Webapp webapp, int defaultFontSize); 41 | 42 | // override this if cleanup of app data needs to be done 43 | def void deleteWebappData(Context context, long webappId) { 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/dlg_open_url.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 16 | 17 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/values/sqlmaps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | create table webapps ( 5 | id integer primary key, 6 | name text not null, 7 | url text not null, 8 | fontSize integer default -1, 9 | userAgent text, 10 | certIssuedBy text, 11 | certIssuedTo text, 12 | certValidFrom text, 13 | certValidTo text, 14 | certHash text, 15 | cookies text 16 | ); 17 | 18 | create table domain_names ( 19 | id integer primary key, 20 | webappId integer not null, 21 | domain text not null 22 | ); 23 | 24 | insert into webapps (name,url) values (\'SuperSport Highlights\',\'https://www.supersport.com/video/\'); 25 | insert into webapps (name,url) values (\'Radio.net\',\'https://www.radio.net\'); 26 | insert into webapps (name,url) values (\'SoundCloud\',\'https://www.soundcloud.com\'); 27 | insert into webapps (name,url) values (\'Mixcloud\',\'https://m.mixcloud.com\'); 28 | insert into webapps (name,url) values (\'Twitch TV\',\'https://m.twitch.tv\'); 29 | 30 | 31 | 32 | select * from webapps 33 | order by name asc 34 | 35 | 36 | 37 | select * from domain_names 38 | where webappId = #webappId# 39 | 40 | 41 | 42 | delete from domain_names 43 | where webappId = #webappId# 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tobykurien/webmediashare/test/DomainTest.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webapps.test 2 | 3 | import com.tobykurien.webapps.webviewclient.WebClient 4 | import org.junit.Test 5 | 6 | import static org.junit.Assert.* 7 | import android.net.Uri 8 | 9 | // Test the critital domain handling code 10 | class DomainTest { 11 | @Test 12 | def void testGetHost() { 13 | assertEquals(WebClient.getHost("tobykurien.com"), "tobykurien.com") 14 | assertEquals(WebClient.getHost("tobykurien.com/something"), "tobykurien.com") 15 | assertEquals(WebClient.getHost("http://tobykurien.com/something"), "tobykurien.com") 16 | assertEquals(WebClient.getHost("https://tobykurien.com:8080/something"), "tobykurien.com") 17 | } 18 | 19 | @Test 20 | def void testRootDomain() { 21 | assertEquals(WebClient.getRootDomain("www.tobykurien.com"), "tobykurien.com") 22 | assertEquals(WebClient.getRootDomain("www.tobykurien.co.za"), "tobykurien.co.za") 23 | assertEquals(WebClient.getRootDomain("www.tobykurien.org.za"), "tobykurien.org.za") 24 | assertEquals(WebClient.getRootDomain("fast.ai"), "fast.ai") 25 | assertEquals(WebClient.getRootDomain("www.fast.ai"), "fast.ai") 26 | } 27 | 28 | @Test 29 | def void testIsInSandbox() { 30 | var domainUrls = #[ "tobykurien.com", "tobykurien.co.za", "tobykurien.org.za" ].toSet 31 | assertTrue(WebClient.isInSandbox(Uri.parse("https://cloud.tobykurien.com"), domainUrls)) 32 | assertTrue(WebClient.isInSandbox(Uri.parse("https://www.tobykurien.co.za"), domainUrls)) 33 | assertTrue(WebClient.isInSandbox(Uri.parse("https://www.tobykurien.org.za"), domainUrls)) 34 | assertFalse(WebClient.isInSandbox(Uri.parse("https://www.test.co.za"), domainUrls)) 35 | assertFalse(WebClient.isInSandbox(Uri.parse("https://www.test.org.za"), domainUrls)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Default 5 | Custom 6 | Desktop (Windows 10, Chrome) 7 | Phone (Android 9, Chrome) 8 | Tablet (Android 7.1.1, Chrome) 9 | iPhone (Safari 13.1) 10 | iPad (Safari 13) 11 | 12 | 13 | 14 | 15 | Custom 16 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 17 | Mozilla/5.0 (Linux; Android 9; SM-G950F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/85.0.4183.81 Mobile Safari/537.36 18 | Mozilla/5.0 (Linux; Android 7.1.1; SM-T555 Build/NMF26X; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/85.0.4183.81 Safari/537.36 19 | Mozilla/5.0 (iPhone; CPU iPhone OS 13_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Mobile/15E148 Safari/604.1 20 | Mozilla/5.0 (iPad; CPU OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1 21 | 22 | 23 | 24 | Smallest 25 | Smaller 26 | Normal 27 | Larger 28 | Largest 29 | 30 | 31 | 32 | 0 33 | 1 34 | 2 35 | 3 36 | 4 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | maven { 5 | url "https://maven.google.com" 6 | } 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.1.2' 11 | classpath 'org.xtext:xtext-android-gradle-plugin:2.0.8' 12 | } 13 | } 14 | 15 | apply plugin: 'com.android.application' 16 | apply plugin: 'org.xtext.android.xtend' 17 | 18 | repositories { 19 | jcenter() 20 | maven { 21 | url "https://maven.google.com" 22 | } 23 | } 24 | 25 | android { 26 | compileSdkVersion 29 27 | buildToolsVersion '29.0.3' 28 | 29 | defaultConfig { 30 | minSdkVersion 14 31 | targetSdkVersion 29 32 | versionCode 4 33 | versionName "v1.4" 34 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 35 | } 36 | 37 | dependencies { 38 | compile 'com.android.support:support-v4:28.0.0' 39 | compile 'com.android.support:appcompat-v7:28.0.0' 40 | compile 'org.eclipse.xtend:org.eclipse.xtend.lib:2.13.0' 41 | compile 'com.github.tobykurien:xtendroid:0.13' 42 | compile 'com.github.bumptech.glide:glide:3.8.0' 43 | 44 | testCompile 'junit:junit:4.13' 45 | androidTestCompile 'com.android.support.test:runner:1.0.2' 46 | androidTestCompile 'com.android.support:support-annotations:28.0.0' 47 | } 48 | 49 | buildTypes { 50 | debug { 51 | applicationIdSuffix '.debug' 52 | versionNameSuffix '-DEBUG' 53 | } 54 | } 55 | 56 | packagingOptions { 57 | exclude 'META-INF/eclipse.inf' 58 | exclude 'META-INF/ECLIPSE_.SF' 59 | exclude 'META-INF/ECLIPSE_.RSA' 60 | } 61 | 62 | lintOptions { 63 | abortOnError false // because missing translations... 64 | } 65 | } 66 | 67 | task runApp(type: Exec) { 68 | commandLine '/usr/local/bin/adb', 'shell', 'monkey -p com.tobykurien.webmediashare.debug -c android.intent.category.LAUNCHER 1' 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 29 | 30 | 34 | 35 | 36 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/utils/CertificateUtils.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.utils 2 | 3 | import android.net.http.SslCertificate 4 | import com.tobykurien.webmediashare.data.Webapp 5 | import com.tobykurien.webmediashare.db.DbService 6 | import java.io.UnsupportedEncodingException 7 | import java.security.MessageDigest 8 | import java.security.NoSuchAlgorithmException 9 | 10 | class CertificateUtils { 11 | def static String SHA1(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException { 12 | var md = MessageDigest.getInstance("SHA-1"); 13 | var textBytes = text.getBytes("iso-8859-1"); 14 | md.update(textBytes, 0, textBytes.length); 15 | var sha1hash = md.digest(); 16 | return sha1hash.map[ Integer.toHexString(it) ].join(); 17 | } 18 | 19 | // Create a hash of the certificate for comparison 20 | def static String certificateHash(SslCertificate certificate) { 21 | SHA1(certificate.issuedBy.DName + 22 | certificate.issuedTo.DName) 23 | } 24 | 25 | // Create a hash of the webapp's saved certificate details for comparison 26 | def static String certificateHash(Webapp webapp) { 27 | SHA1(webapp.certIssuedBy + 28 | webapp.certIssuedTo) 29 | } 30 | 31 | def static int compare(SslCertificate cert1, SslCertificate cert2) { 32 | cert1.certificateHash.compareTo(cert2.certificateHash) 33 | } 34 | 35 | def static int compare(Webapp webapp, SslCertificate cert2) { 36 | webapp.certificateHash.compareTo(cert2.certificateHash) 37 | } 38 | 39 | // Save the certificate details to the webapp 40 | def static void updateCertificate(Webapp webapp, SslCertificate certificate, DbService db) { 41 | if (certificate == null || certificate.issuedBy == null || 42 | certificate.issuedTo == null) return; 43 | 44 | db.update(DbService.TABLE_WEBAPPS, #{ 45 | 'certIssuedBy' -> certificate.issuedBy.DName, 46 | 'certIssuedTo' -> certificate.issuedTo.DName, 47 | 'certValidFrom' -> certificate.validNotBefore, 48 | 'certValidTo' -> certificate.validNotAfter 49 | }, webapp.id) 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/adapter/WebappsAdapter.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.adapter 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import com.bumptech.glide.Glide 8 | import com.tobykurien.webmediashare.R 9 | import com.tobykurien.webmediashare.data.Webapp 10 | import com.tobykurien.webmediashare.utils.FaviconHandler 11 | import java.io.File 12 | import java.util.List 13 | import org.xtendroid.adapter.AndroidAdapter 14 | import org.xtendroid.adapter.AndroidViewHolder 15 | import android.widget.BaseAdapter 16 | import com.bumptech.glide.load.engine.DiskCacheStrategy 17 | import java.net.URL 18 | import com.bumptech.glide.signature.StringSignature 19 | 20 | /** 21 | * Android adapter to display webapps using the row_webapp layout 22 | */ 23 | @AndroidAdapter class WebappsAdapter { 24 | List webapps 25 | FaviconHandler favicoHandler 26 | 27 | /** 28 | * ViewHolder class to save references to UI widgets in each row 29 | */ 30 | @AndroidViewHolder(R.layout.row_webapp) static class ViewHolder { 31 | } 32 | 33 | override getView(int row, View cv, ViewGroup parent) { 34 | var vh = ViewHolder.getOrCreate(context, cv, parent) 35 | var app = getItem(row) 36 | 37 | vh.name.text = app.name 38 | vh.url.text = new URL(app.url).host 39 | 40 | if (favicoHandler == null) favicoHandler = new FaviconHandler(context) 41 | var favico = favicoHandler.getFavIcon(app.id) 42 | loadFavicon(context, favico, vh.favicon) 43 | 44 | return vh.view 45 | } 46 | 47 | /** 48 | * Load a favicon into an imageview 49 | */ 50 | def static loadFavicon(Context context, File favico, ImageView view) { 51 | if (favico.exists) { 52 | Glide.with(context) 53 | .load(favico) 54 | .diskCacheStrategy(DiskCacheStrategy.NONE) 55 | .centerCrop() 56 | .placeholder(R.drawable.ic_action_site) 57 | .crossFade() 58 | .into(view); 59 | } else { 60 | view.imageResource = R.drawable.ic_action_site 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | サイトを変更 4 | 終了 5 | フォントサイズ 6 | 停止 7 | 更新 8 | 画像の切り替え 9 | 設定 10 | サードパーティードメインをブロックしました 11 | URL を開く 12 | Webアプリとして保存 13 | URL を共有 14 | ホーム画面のショートカット 15 | ユーザーエージェント 16 | 証明書 17 | 18 | site.com 19 | URL を開く 20 | 無効な URL 21 | 22 | ルートドメインをブロックしました 23 | ブロック解除 24 | Webアプリを保存 25 | 現在のサイトを Webアプリとして保存しますか? 26 | Webアプリの保存中 27 | ショートカットを追加しました 28 | 保存 29 | Webアプリ名 30 | サイトを開く 31 | Webアプリを削除しますか? 32 | Webアプリのショートカット 33 | Webapp not found 34 | 35 | Webサイト証明書 36 | 信頼できない証明書 37 | 証明書が変更されました 38 | 証明書を受け入れ 39 | 40 | ヒント 41 | ホーム画面のショートカット: ホーム画面を長押ししてウィジェットページにアクセスし、ホーム画面にWebアプリのショートカットを追加します

43 |

リンクの長押し: Webサイトのリンクを長押しすると、リンクを開く方法を選択できます

44 | ]]>
45 |
46 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/fragment/DlgCertificateChanged.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.fragment 2 | 3 | import android.net.http.SslCertificate 4 | import com.tobykurien.webmediashare.data.Webapp 5 | import org.xtendroid.annotations.AndroidDialogFragment 6 | import com.tobykurien.webmediashare.R 7 | import org.xtendroid.app.OnCreate 8 | import android.os.Bundle 9 | import android.support.v7.app.AlertDialog 10 | 11 | @AndroidDialogFragment(R.layout.dlg_certificate_changed) class DlgCertificateChanged extends DlgCertificate { 12 | var Webapp webapp = null 13 | 14 | public new(Webapp webapp, SslCertificate certificate, String title, String okText, 15 | ()=>boolean onOkClicked, ()=>boolean onCancelClicked) { 16 | super(certificate, title, okText, onOkClicked, onCancelClicked) 17 | this.webapp = webapp 18 | } 19 | 20 | /** 21 | * Create a dialog using the AlertDialog Builder, but our custom layout 22 | */ 23 | override onCreateDialog(Bundle instance) { 24 | if (title == null) title = getString(R.string.title_certificate) 25 | 26 | new AlertDialog.Builder(activity) 27 | .setTitle(title) 28 | .setView(contentView) // contentView is the layout specified in the annotation 29 | .setPositiveButton( 30 | if (okText == null) getString(android.R.string.ok) else okText, 31 | [ if (onOkClicked != null) onOkClicked.apply() ]) // to avoid it closing dialog 32 | .setNegativeButton(android.R.string.cancel, [ 33 | if (onCancelClicked != null) onCancelClicked.apply() 34 | ]) 35 | .create() 36 | } 37 | 38 | @OnCreate 39 | override init() { 40 | issuedBy1.text = webapp.certIssuedBy.formatDname 41 | issuedTo1.text = webapp.certIssuedTo.formatDname 42 | expires1.text = webapp.certValidFrom + " to \n" + webapp.certValidTo 43 | 44 | issuedBy2.text = certificate.issuedBy.DName.formatDname 45 | issuedTo2.text = certificate.issuedTo.DName.formatDname 46 | expires2.text = certificate.validNotBeforeDate.toLocaleString + " to \n" + 47 | certificate.validNotAfterDate.toLocaleString 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/fragment/DlgCertificate.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.fragment 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AlertDialog 5 | import org.xtendroid.annotations.AndroidDialogFragment 6 | import org.xtendroid.app.OnCreate 7 | import android.support.v4.app.DialogFragment 8 | import com.tobykurien.webmediashare.R 9 | import android.net.http.SslCertificate 10 | import com.tobykurien.webmediashare.data.Webapp 11 | 12 | @AndroidDialogFragment(R.layout.dlg_certificate) class DlgCertificate extends DialogFragment { 13 | var protected SslCertificate certificate = null 14 | var protected String title = null 15 | var protected String okText = null 16 | var protected ()=>boolean onOkClicked = null 17 | var protected ()=>boolean onCancelClicked = null 18 | 19 | public new(SslCertificate certificate, String title, String okText, 20 | ()=>boolean onOkClicked, ()=>boolean onCancelClicked) { 21 | this.certificate = certificate 22 | this.title = title 23 | this.okText = okText 24 | this.onOkClicked = onOkClicked 25 | this.onCancelClicked = onCancelClicked 26 | } 27 | 28 | public new(SslCertificate certificate) { 29 | this.certificate = certificate 30 | } 31 | 32 | /** 33 | * Create a dialog using the AlertDialog Builder, but our custom layout 34 | */ 35 | override onCreateDialog(Bundle instance) { 36 | if (title == null) title = getString(R.string.title_certificate) 37 | 38 | new AlertDialog.Builder(activity) 39 | .setTitle(title) 40 | .setView(contentView) // contentView is the layout specified in the annotation 41 | .setPositiveButton( 42 | if (okText == null) getString(android.R.string.ok) else okText, 43 | [ if (onOkClicked != null) onOkClicked.apply() ]) // to avoid it closing dialog 44 | .setNegativeButton(android.R.string.cancel, [ 45 | if (onCancelClicked != null) onCancelClicked.apply() 46 | ]) 47 | .create() 48 | } 49 | 50 | @OnCreate 51 | def init() { 52 | issuedBy.text = certificate.issuedBy.DName.formatDname 53 | issuedTo.text = certificate.issuedTo.DName.formatDname 54 | expires.text = certificate.validNotBeforeDate.toLocaleString + " to \n" + 55 | certificate.validNotAfterDate.toLocaleString 56 | } 57 | 58 | def static formatDname(String DName) { 59 | DName.replace("\\,", " ").split(",").join("\n") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/res/xml/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 19 | 20 | 28 | 29 | 34 | 35 | 41 | 42 | 47 | 48 | 53 | 54 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebMediaShare - web media your way! 2 | ============= 3 | 4 | **DEPRECTATED**: This project is no longer maintained. Reasons are explained in [this issue](https://github.com/tobykurien/WebApps/issues/253) 5 | 6 | ![Main screen](images/wms1.png) ![Viewing media](images/wms2.png) ![Sharing media](images/wms3.png) 7 | 8 | 9 | WebMediaShare is an app to browse your favourite media websites (e.g. online streaming sites, online radio stations, etc.) so that you can: 10 | 11 | - view the content without ads/popups/redirects/etc. 12 | - listen to music from a streaming site in a media player app like VLC, so that it continues playing even if the screen is off 13 | - send the media to your TV or Hifi (e.g. via the Kore app for Kodi). This works like Chromecast, but without the need for the Chromecast device, a Google account or Google Play Services, or special support for the site. 14 | - share the media URL to friends on chats or email 15 | - share the media to an app for downloading 16 | 17 | WebMediaShare is a browser with the following features: 18 | 19 | - Save your favourite media sites in-app 20 | - Add shortcuts to the home screen so that they open like regular apps 21 | - Ad blocking 22 | - Prevents popups, pop-unders, and redirects (i.e. works well with sports and other live streaming sites) 23 | - intercepts media within web pages, allowing you to view and share them 24 | 25 | Forked from WebApps [https://github.com/tobykurien/WebApps](https://github.com/tobykurien/WebApps), which is a more privacy-oriented app that works well for social media and other web apps. 26 | 27 | 28 | 29 | 30 | 31 | [Get the APK from releases](https://github.com/tobykurien/WebMediaShare/releases) 32 | 33 | Limitations 34 | =========== 35 | 36 | - Cookies and referer information is lost when sharing a media URL, so it may not work on some sites if the server requires these. 37 | - Casting does not work well for sites like YouTube.com that stream their media in several chunked files 38 | - For YouTube in particular, use "Share URL" menu option to share to Kore. Your Kodi instance will need the YouTube plugin installed. 39 | - Another alternative is to use an [Invidious](https://github.com/iv-org/invidious) instance for YouTube, which works well and removes some YouTube restrictions and allows even streaming just the audio or video independently. 40 | - Casting may not work on sites that implement DRM, e.g. F1TV or Strikeout 41 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Seite wechseln 4 | Webseite schließen 5 | Schriftgröße 6 | Stop 7 | Neu laden 8 | Bilder ein/aus 9 | Einstellungen 10 | Geblockte Drittanbieter Domains 11 | URL öffnen 12 | Als Webapp speichern 13 | URL teilen 14 | Verknüpfung auf Startbildschirm 15 | User Agent 16 | Zertifikat 17 | 18 | webseite.de 19 | URL öffnen 20 | Ungültige URL 21 | 22 | Geblockte Root Domains 23 | Entsperren 24 | Speichere Webapp 25 | Aktuelle Seite als Webapp speichern? 26 | Speichere Webapp 27 | Verknüpfung hinzugefügt 28 | Speichern 29 | Webapp Name 30 | Seite öffnen 31 | Webapp löschen? 32 | Webapp Verknüpfung 33 | Webapp nicht gefunden 34 | 35 | Zertifikat 36 | Nicht vertrauenswürdiges Zertifikat 37 | Zertifikat wurde geändert 38 | Zertifikat akzeptieren 39 | 40 | Tipps 41 | Verknüpfung auf Startbildschirm: Auf den Startbildschirm des Geräts lange drücken und über die Widget Seite eine Verknüpfung zu einer Webapp hinzufügen.

43 |

Links öffnen: Auf einen Link lange drücken, um zu bestimmen, wie er geöffnet wird.

44 | ]]>
45 |
46 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | Changer de site 13 | Quitter 14 | Taille de police 15 | Arrêter 16 | Rafraichir 17 | Changer d\'image 18 | Paramètres 19 | Bloquer les domaines tiers 20 | Ouvrir l\'URL 21 | site-web.fr 22 | Ouvrir l\'URL 23 | Sauvegarder comme WebApp 24 | Bloqué les domaines racines 25 | Débloquer 26 | Sauvegarder la WebApp 27 | Sauvegarder le site web actuel comme WebbApp ? 28 | open_url_hint 29 | Nom de la WebApp 30 | Ouvrir le site web 31 | Supprimer la WebApp ? 32 | 33 | URL invalide 34 | Sauvegarde de la webapp 35 | Raccourci pour la webapp 36 | 37 | Astuces 38 | Raccourcis de l\'écran d\'accueil : Faites un appui long sur l\'écran d\'accueil pour accéder aux widgets pour ajouter un raccourci vers une webapp sur l\'écran d\'accueil

40 |

Appui long sur les liens : Faites un appui long sur un lien sur un site web pour choisir comment l\'ouvrir

41 | ]]>
42 | Webapp pas trouvé 43 | Partager l\'URL 44 | Raccourci 45 | Raccourci ajouté 46 |
47 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/activity/ShortcutActivity.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.activity 2 | 3 | import android.content.Intent 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.net.Uri 7 | import android.support.v4.content.pm.ShortcutInfoCompat 8 | import android.support.v4.content.pm.ShortcutManagerCompat 9 | import android.support.v4.graphics.drawable.IconCompat 10 | import android.view.Menu 11 | import com.tobykurien.webmediashare.R 12 | import com.tobykurien.webmediashare.utils.FaviconHandler 13 | import android.content.Context 14 | import com.tobykurien.webmediashare.data.Webapp 15 | 16 | /** 17 | * Activity to allow the user to pick a webapp when creating a new shortcut 18 | */ 19 | class ShortcutActivity extends MainActivity { 20 | 21 | override protected onStart() { 22 | super.onStart() 23 | 24 | mainList.setOnItemClickListener([ av, v, pos, id | 25 | var shortcut = getShortcut(this, webapps.get(pos)) 26 | var ret = ShortcutManagerCompat.createShortcutResultIntent(this, shortcut.build()) 27 | setResult(RESULT_OK, ret); 28 | 29 | finish() 30 | ]) 31 | 32 | mainList.onItemLongClickListener = [true] 33 | } 34 | 35 | def static getShortcut(Context context, Webapp webapp) { 36 | // Adding shortcut on Home screen 37 | var launchIntent = new Intent(context, WebAppActivity); 38 | launchIntent.action = Intent.ACTION_VIEW 39 | launchIntent.data = Uri.parse(webapp.url) 40 | BaseWebAppActivity.putWebappId(launchIntent, webapp.id) 41 | 42 | var shortcut = new ShortcutInfoCompat.Builder(context, webapp.name) 43 | .setIntent(launchIntent) 44 | .setShortLabel(webapp.name) 45 | 46 | var size = context.getResources().getDimension(android.R.dimen.app_icon_size) as int; 47 | var favicon = new FaviconHandler(context).getFavIcon(webapp.id); 48 | 49 | if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { 50 | if(favicon.exists) { 51 | var icon = IconCompat.createWithBitmap( 52 | Bitmap.createScaledBitmap(BitmapFactory.decodeFile(favicon.path), size, size, false)) 53 | shortcut.setIcon(icon); 54 | } else { 55 | var icon = IconCompat.createWithResource(context, R.drawable.ic_action_site) 56 | shortcut.setIcon(icon); 57 | } 58 | } 59 | 60 | return shortcut 61 | } 62 | 63 | override onCreateOptionsMenu(Menu menu) { 64 | return false 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dlg_certificate.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 13 | 14 | 18 | 19 | 25 | 26 | 30 | 31 | 32 | 36 | 37 | 42 | 43 | 47 | 48 | 49 | 53 | 54 | 59 | 60 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/res/menu/webapps_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | 24 | 25 | 30 | 31 | 36 | 37 | 42 | 43 | 48 | 49 | 54 | 55 | 60 | 61 | 68 | 69 | 74 | 75 | 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/webviewclient/WebViewUtilsApi11.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.webviewclient 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.webkit.CookieManager 7 | import android.webkit.CookieSyncManager 8 | import android.webkit.WebIconDatabase 9 | import android.webkit.WebSettings 10 | import android.webkit.WebSettings.PluginState 11 | import android.webkit.WebSettings.TextSize 12 | import android.webkit.WebView 13 | import com.tobykurien.webmediashare.data.Webapp 14 | import com.tobykurien.webmediashare.utils.Settings 15 | import android.annotation.TargetApi 16 | 17 | import static extension com.tobykurien.webmediashare.utils.Dependencies.* 18 | 19 | @TargetApi(11) 20 | class WebViewUtilsApi11 extends WebViewUtils { 21 | 22 | override void setupWebView(Context context, WebView wv, 23 | Uri siteUrl, Webapp webapp, int defaultFontSize) { 24 | WebIconDatabase.getInstance().open( 25 | context.getDir("icons", Context.MODE_PRIVATE).getPath()); 26 | CookieSyncManager.createInstance(context); 27 | CookieManager.getInstance().setAcceptCookie(true); 28 | 29 | var settings = wv.getSettings(); 30 | settings.setJavaScriptEnabled(true); 31 | settings.setJavaScriptCanOpenWindowsAutomatically(false); 32 | 33 | // Enable local database per site 34 | // NOTE: No longer works on API 19+ 35 | settings.setDatabaseEnabled(true); 36 | var databasePath = context.getApplicationContext().getCacheDir() 37 | + "db-" + WebClient.getHost(siteUrl); 38 | settings.setDatabasePath(databasePath); 39 | 40 | // Enable caching each site individually 41 | // NOTE: No longer works on API 19+ 42 | var cachePath = context.getApplicationContext().getCacheDir() 43 | + "/cache-" + WebClient.getHost(siteUrl); 44 | settings.setAppCachePath(cachePath); 45 | settings.setAppCacheEnabled(true); 46 | settings.setAppCacheMaxSize(1024 * 1024 * 8); 47 | settings.setCacheMode(WebSettings.LOAD_DEFAULT); 48 | 49 | // allow access to documents for upload 50 | settings.allowContentAccess = true 51 | settings.allowFileAccess = true 52 | 53 | settings.setPluginState(PluginState.OFF); 54 | settings.setDomStorageEnabled(true); 55 | settings.setSupportZoom(true); 56 | settings.setBuiltInZoomControls(false); 57 | settings.setGeolocationEnabled(true); // allow maps, etc. to work 58 | settings.setJavaScriptCanOpenWindowsAutomatically(false); 59 | settings.setSaveFormData(false); 60 | settings.setSavePassword(false); 61 | settings.setLoadsImagesAutomatically(context.settings.isLoadImages()); 62 | 63 | // set preferred text size 64 | if (webapp.getFontSize() >= 0) { 65 | setTextSize(wv, webapp.getFontSize()); 66 | } else { 67 | setTextSize(wv, defaultFontSize); 68 | } 69 | 70 | // set preferred user agent 71 | var userAgent = context.settings.getUserAgent(); 72 | if (webapp.userAgent != null && webapp.userAgent.trim.length > 0) { 73 | userAgent = webapp.userAgent 74 | } 75 | if (userAgent != null && userAgent.trim.length > 0) { 76 | wv.getSettings().setUserAgentString(userAgent); 77 | } 78 | 79 | wv.addJavascriptInterface([ 80 | throw new IllegalStateException("not supported"); 81 | ], "window"); 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/fragment/DlgSaveWebapp.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.fragment 2 | 3 | import android.app.ProgressDialog 4 | import android.net.http.SslCertificate 5 | import android.os.Bundle 6 | import android.support.v4.app.DialogFragment 7 | import android.support.v7.app.AlertDialog 8 | import android.util.Log 9 | import com.tobykurien.webmediashare.R 10 | import com.tobykurien.webmediashare.data.Webapp 11 | import com.tobykurien.webmediashare.db.DbService 12 | import java.util.Set 13 | import org.eclipse.xtext.xbase.lib.Functions.Function1 14 | import org.xtendroid.annotations.AndroidDialogFragment 15 | import org.xtendroid.utils.AsyncBuilder 16 | 17 | import static extension com.tobykurien.webmediashare.utils.Dependencies.* 18 | import static extension org.xtendroid.utils.AlertUtils.* 19 | 20 | /** 21 | * Dialog to save a Webapp. 22 | */ 23 | @AndroidDialogFragment(R.layout.dlg_save) class DlgSaveWebapp extends DialogFragment { 24 | long webappId 25 | var String title 26 | var String url 27 | var Set unblock 28 | var Function1 onSave 29 | var SslCertificate certificate 30 | 31 | public new(long webappId, String title, String url, SslCertificate certificate, Set unblock) { 32 | this.webappId = webappId 33 | this.title = title 34 | this.url = url 35 | this.unblock = unblock 36 | this.certificate = certificate 37 | } 38 | 39 | /** 40 | * Create a dialog using the AlertDialog Builder, but our custom layout 41 | */ 42 | override onCreateDialog(Bundle instance) { 43 | new AlertDialog.Builder(activity) 44 | .setTitle(R.string.title_save_webapp) 45 | .setView(contentView) // contentView is the layout specified in the annotation 46 | .setPositiveButton(android.R.string.ok, null) // to avoid it closing dialog 47 | .setNegativeButton(android.R.string.cancel, null) 48 | .create() 49 | } 50 | 51 | override onStart() { 52 | super.onStart() 53 | 54 | val button = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) 55 | button.setOnClickListener [ 56 | onSaveClick() 57 | ] 58 | 59 | name.text = title 60 | } 61 | 62 | def void onSaveClick() { 63 | val pd = new ProgressDialog(activity) 64 | pd.message = getString(R.string.msg_saving_webapp) 65 | 66 | AsyncBuilder.async(pd) [builder, params| 67 | val values = #{ 68 | "name" -> name.text.toString, 69 | "url" -> this.url 70 | } 71 | 72 | if (webappId >= 0) { 73 | activity.db.update(DbService.TABLE_WEBAPPS, values, 74 | String.valueOf(webappId)); 75 | } else { 76 | webappId = activity.db.insert(DbService.TABLE_WEBAPPS, values); 77 | } 78 | 79 | // NOTE: saving of unblock list moved to the 3rdparty dialog 80 | 81 | return activity.db.findById(DbService.TABLE_WEBAPPS, webappId, Webapp); 82 | ].then[ result | 83 | dismiss 84 | if (result === null) throw new Exception("Webapp did not save to database") 85 | if (onSave != null) { 86 | onSave.apply(result) 87 | } 88 | ].onError[Exception err| 89 | Log.e("dlg_save", "error saving webapp", err) 90 | activity.toast(err.class.name + ": " + err.message) 91 | ].start() 92 | } 93 | 94 | def void setOnSaveListener(Function1 listener) { 95 | onSave = listener 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/fragment/DlgShareMedia.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.fragment 2 | 3 | import android.support.v4.app.DialogFragment 4 | import org.xtendroid.annotations.AndroidDialogFragment 5 | import com.tobykurien.webmediashare.R 6 | import android.support.v7.app.AlertDialog 7 | import android.net.Uri 8 | import com.tobykurien.webmediashare.webviewclient.WebClient 9 | import android.os.Bundle 10 | import java.util.List 11 | import com.tobykurien.webmediashare.data.MediaUrl 12 | import android.content.Intent 13 | import android.util.Log 14 | import com.tobykurien.webmediashare.adapter.MediaUrlsAdapter 15 | import android.content.Context 16 | import android.support.v4.content.LocalBroadcastManager 17 | import android.content.IntentFilter 18 | 19 | @AndroidDialogFragment class DlgShareMedia extends DialogFragment { 20 | val List mediaUrls 21 | var MediaUrlsAdapter adapter = null 22 | var MediaUrl selectedMediaUrl = null 23 | 24 | val mediaUrlReceiver = new android.content.BroadcastReceiver() { 25 | override onReceive(Context context, Intent intent) { 26 | adapter?.notifyDataSetChanged() 27 | } 28 | } 29 | 30 | new () { 31 | super() 32 | mediaUrls = null 33 | if (true) throw new IllegalAccessException("Use the contructor with mediaUrls") 34 | } 35 | 36 | new(List inMediaUrls) { 37 | super() 38 | this.mediaUrls = inMediaUrls 39 | } 40 | 41 | /** 42 | * Create a dialog using the AlertDialog Builder, but our custom layout 43 | */ 44 | override onCreateDialog(Bundle instance) { 45 | adapter = new MediaUrlsAdapter(activity, mediaUrls) 46 | selectedMediaUrl = mediaUrls.get(0) 47 | 48 | new AlertDialog.Builder(activity) 49 | .setTitle(R.string.title_share_media) 50 | .setSingleChoiceItems(adapter, 0, [a, b| 51 | selectedMediaUrl = mediaUrls.get(b) 52 | ]) 53 | .setPositiveButton(R.string.btn_share_url, null) // to avoid it closing dialog 54 | .setNeutralButton(R.string.btn_share_stream,null) 55 | //.setNegativeButton(android.R.string.cancel, null) 56 | .create() 57 | } 58 | 59 | override onStart() { 60 | super.onStart() 61 | 62 | // register to listen for media URL broadcasts 63 | LocalBroadcastManager.getInstance(activity).registerReceiver(mediaUrlReceiver, 64 | new IntentFilter(WebClient.MEDIA_URL_FOUND)) 65 | 66 | Log.d("DlgShareMedia", mediaUrls.toString) 67 | if (mediaUrls == null || mediaUrls.length == 0) { 68 | dismiss() 69 | return 70 | } 71 | 72 | val button1 = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) 73 | button1.setOnClickListener [ 74 | val i = new Intent(Intent.ACTION_SEND); 75 | i.setType("text/plain") 76 | i.putExtra(Intent.EXTRA_TEXT, selectedMediaUrl.uri.toString()); 77 | i.putExtra(Intent.EXTRA_SUBJECT, selectedMediaUrl.uri.host); 78 | i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 79 | i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 80 | var chooser = Intent.createChooser(i, selectedMediaUrl.uri.host) 81 | if (i.resolveActivity(activity.getPackageManager()) != null) { 82 | activity.startActivity(chooser); 83 | } 84 | ] 85 | 86 | val button2 = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEUTRAL) 87 | button2.setOnClickListener [ 88 | val i = new Intent(Intent.ACTION_VIEW, selectedMediaUrl.uri); 89 | i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 90 | i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 91 | var chooser = Intent.createChooser(i, selectedMediaUrl.uri.host) 92 | if (i.resolveActivity(activity.getPackageManager()) != null) { 93 | activity.startActivity(chooser); 94 | } 95 | ] 96 | } 97 | 98 | override onStop() { 99 | LocalBroadcastManager.getInstance(activity).unregisterReceiver(mediaUrlReceiver) 100 | 101 | super.onStop() 102 | } 103 | 104 | 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Web Media Share 4 | Change Site 5 | Exit 6 | Font Size 7 | Stop 8 | Refresh 9 | Toggle Images 10 | Share Media 11 | Settings 12 | 3rd party domains 13 | Open URL 14 | Save as Webapp 15 | Share URL 16 | Homescreen shortcut 17 | User Agent 18 | Certificate 19 | 20 | site.com 21 | Open URL 22 | Invalid URL 23 | 24 | Unblock 3rd party domains 25 | Unblock 26 | Save Webapp 27 | Save current site as a Webapp? 28 | Saving webapp 29 | Shortcut added 30 | Save 31 | Webapp Name 32 | Open Site 33 | Recommended 34 | Share URL 35 | View Stream 36 | Delete Webapp? 37 | Webapp shortcut 38 | Webapp not found 39 | Open With 40 | Share Media 41 | Popup Blocked 42 | 43 | Website Certificate 44 | Untrusted Certificate 45 | Certificate Changed 46 | Accept Certificate 47 | 48 | Tips 49 | Adding web apps: to add a new web app, open the site, then click the "Save as Webapp" action

51 |

Casting: Click the cast action to send media to other apps like VLC or Kodi remote (Kore) for playing on your TV

52 |

Homescreen shortcuts: Long-press your homescreen and access the widgets page to add a webapp shortcut to your homescreen

53 | ]]>
54 | Source code 55 | 56 | Image loading enabled 57 | Image loading disabled 58 | Opening web app... 59 | Open in new sandbox 60 | 61 | 62 | Block 3rd party requests 63 | Don\'t load images/scripts outside webapps domains 64 | Font Size 65 | Change the rendered font size 66 | User Agent 67 | Affects how sites render on your device 68 | Fullscreen 69 | Hide the status bar (requires restart) 70 | Immersive mode 71 | In fullscreen mode, hide all system UI 72 | Auto-hide action bar 73 | Hide the action bar when you scroll down, reveal when you scroll up 74 | Hide actionbar 75 | Remove the actionbar completely. Long-press a blank spot to toggle. 76 | Hide only for shortcuts 77 | Only hide the actionbar when launched from a shortcut 78 |
79 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/fragment/DlgOpenUrl.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.fragment 2 | 3 | import android.app.ProgressDialog 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.support.v4.app.DialogFragment 8 | import android.support.v7.app.AlertDialog 9 | import android.util.Log 10 | import android.webkit.CookieManager 11 | import com.tobykurien.webmediashare.R 12 | import com.tobykurien.webmediashare.activity.BaseWebAppActivity 13 | import com.tobykurien.webmediashare.activity.WebAppActivity 14 | import com.tobykurien.webmediashare.data.Webapp 15 | import java.io.InputStream 16 | import java.net.URL 17 | import java.net.URLConnection 18 | import org.xtendroid.annotations.AndroidDialogFragment 19 | 20 | import static org.xtendroid.utils.AsyncBuilder.* 21 | 22 | import static extension com.tobykurien.webmediashare.utils.Dependencies.* 23 | import static extension org.xtendroid.utils.AlertUtils.* 24 | import android.content.Context 25 | import android.webkit.CookieSyncManager 26 | import android.os.Build 27 | import com.tobykurien.webmediashare.webviewclient.WebClient 28 | 29 | /** 30 | * Dialog to open a URL. 31 | */ 32 | @AndroidDialogFragment(R.layout.dlg_open_url) class DlgOpenUrl extends DialogFragment { 33 | 34 | /** 35 | * Create a dialog using the AlertDialog Builder, but our custom layout 36 | */ 37 | override onCreateDialog(Bundle instance) { 38 | new AlertDialog.Builder(activity) 39 | .setTitle(R.string.open_site) 40 | .setView(contentView) // contentView is the layout specified in the annotation 41 | .setPositiveButton(android.R.string.ok, null) // to avoid it closing dialog 42 | .setNegativeButton(android.R.string.cancel, null) 43 | // .setNeutralButton(R.string.btn_recommended_sites, [ 44 | // var link = Uri.parse("https://github.com/tobykurien/WebMediaShare/wiki/Recommended-Webapps") 45 | // WebClient.handleExternalLink(activity, link, true); 46 | // dismiss() 47 | // ]) 48 | .create() 49 | } 50 | 51 | override onStart() { 52 | super.onStart() 53 | 54 | val button = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) 55 | button.setOnClickListener [ 56 | if (onOpenUrlClick()) { 57 | dialog.dismiss 58 | } 59 | ] 60 | } 61 | 62 | def boolean onOpenUrlClick() { 63 | var url = txtOpenUrl.text.toString; 64 | try { 65 | openUrl(activity, url, chkNewSandbox.checked) 66 | } catch (Exception e) { 67 | txtOpenUrl.setError(getString(R.string.err_invalid_url), null) 68 | return false 69 | } 70 | 71 | return true 72 | } 73 | 74 | def static openUrl(Context activity, String url, boolean newSandbox) { 75 | var Uri uri = null 76 | try { 77 | if (url.trim().length == 0) throw new Exception(); 78 | 79 | if (!url.contains("://")) { 80 | uri = Uri.parse("http://" + url) 81 | } else { 82 | uri = Uri.parse(url) 83 | } 84 | } catch (Exception e) { 85 | Log.e("dlgOpenUrl", "Error opening url", e) 86 | return false 87 | } 88 | 89 | // When opening a new URL, let's follow all redirects to get to the final destination 90 | val originalUri = uri 91 | val pd = new ProgressDialog(activity) 92 | pd.setMessage(activity.getString(R.string.progress_opening_site)) 93 | 94 | async(pd) [ 95 | var URLConnection con = new URL(originalUri.toString()).openConnection() 96 | if (activity.settings.userAgent != null && 97 | activity.settings.userAgent.trim().length > 0) { 98 | // User-agent may affect site redirects 99 | con.setRequestProperty("User-Agent", activity.settings.userAgent) 100 | } 101 | con.connect() 102 | var InputStream is = con.getInputStream() 103 | var finalUrl = con.getURL() 104 | is.close() 105 | return finalUrl.toString() 106 | ].then [ result | 107 | if (!pd.isShowing) return; // user cancelled 108 | 109 | var Uri uriFinal = null 110 | if (!result.equals(originalUri.toString())) { 111 | uriFinal = Uri.parse(result) 112 | } else { 113 | uriFinal = originalUri 114 | } 115 | 116 | if (newSandbox) { 117 | // open in new sandbox 118 | // delete all previous cookies 119 | CookieManager.instance.removeAllCookie() 120 | var i = new Intent(activity, WebAppActivity) 121 | i.action = Intent.ACTION_VIEW 122 | i.data = uriFinal 123 | activity.startActivity(i) 124 | 125 | } else { 126 | WebClient.handleExternalLink(activity, uriFinal, false, false) 127 | } 128 | ].onError[ Exception error | 129 | Log.e("dlgOpenUrl", "Error", error) 130 | try { 131 | activity.toast(error.message) 132 | } catch (Exception e) { 133 | // ignore, dialog must be dismissed 134 | } 135 | ].start() 136 | 137 | return false 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/utils/FaviconHandler.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.utils 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.graphics.Color 7 | import android.util.Log 8 | import java.io.BufferedOutputStream 9 | import java.io.File 10 | import java.io.FileInputStream 11 | import java.io.FileOutputStream 12 | 13 | import static extension org.xtendroid.utils.TimeUtils.* 14 | 15 | class FaviconHandler { 16 | val Context context 17 | 18 | new(Context context) { 19 | this.context = context 20 | } 21 | 22 | /** 23 | * Retrieves a File handle to favicon for webapp. Ensure that you check File.exists before use 24 | */ 25 | def File getFavIcon(long webappId) { 26 | getFile(webappId) 27 | } 28 | 29 | /** 30 | * Saves bitmap as favicon for specified webapp, if it hasn't been modified in the last 24 hours. 31 | * NOTE: Runs on current thread! 32 | */ 33 | def void saveFavIcon(long webappId, Bitmap icon) { 34 | val f = getFile(webappId) 35 | if (Debug.FAVICON) Log.d("favicon", "Received favicon " + icon.width + "x" + icon.height) 36 | 37 | if (f.exists) { 38 | // make sure new icon is of higher or same resolution 39 | var bmpOpt = new BitmapFactory.Options() 40 | bmpOpt.inJustDecodeBounds = true 41 | BitmapFactory.decodeStream(new FileInputStream(f), null, bmpOpt) 42 | if (Debug.FAVICON) Log.d("favicon", "Icon IN=" + icon.width + "x" + icon.height + ", CACHED=" + bmpOpt.outWidth + "x" + bmpOpt.outHeight) 43 | 44 | if (bmpOpt.outHeight > icon.height && bmpOpt.outWidth > icon.width) { 45 | // new icon is lower res 46 | if (Debug.FAVICON) Log.d("favicon", "Skipping because lower res") 47 | return 48 | } 49 | 50 | if (bmpOpt.outHeight == icon.height && bmpOpt.outWidth == icon.width && 51 | System.currentTimeMillis - f.lastModified < 24.hours) { 52 | // new icon matches saved icon, and it was saved recently, so no need to overwrite 53 | if (Debug.FAVICON) Log.d("favicon", "Skipping because we cached this recently") 54 | return 55 | } 56 | 57 | f.delete() 58 | } 59 | 60 | if (Debug.FAVICON) Log.d("favicon", "Saving new icon for " + webappId) 61 | val os = new BufferedOutputStream(new FileOutputStream(f)) 62 | try { 63 | icon.compress(Bitmap.CompressFormat.PNG, 100, os); 64 | os.flush() 65 | } finally { 66 | os.close() 67 | } 68 | } 69 | 70 | def deleteFavIcon(long webappId) { 71 | try { 72 | val f = getFile(webappId) 73 | if (f.exists) f.delete() 74 | } catch (Exception e) { 75 | Log.e("favicon", "Error deleting icon", e) 76 | } 77 | } 78 | 79 | def private File getFile(long webappId) { 80 | new File(context.cacheDir.path + "/favicon-" + webappId + ".png") 81 | } 82 | 83 | // from: https://stackoverflow.com/questions/8471236/finding-the-dominant-color-of-an-image-in-an-android-drawable 84 | def static int getDominantColor(File image) { 85 | val defaultColor = Color.rgb(0xe8, 0x00, 0xec); 86 | 87 | if (image === null || !image.exists) { 88 | return defaultColor 89 | } 90 | 91 | val bitmap = BitmapFactory.decodeFile(image.absolutePath) 92 | if (bitmap === null) return defaultColor 93 | 94 | val int width = bitmap.getWidth(); 95 | val int height = bitmap.getHeight(); 96 | val int size = width * height; 97 | val int[] pixels = newIntArrayOfSize(size); 98 | //Bitmap bitmap2 = bitmap.copy(Bitmap.Config.ARGB_4444, false); 99 | bitmap.getPixels(pixels, 0, width, 0, 0, width, height); 100 | var int color = 0; 101 | var int r = 0; 102 | var int g = 0; 103 | var int b = 0; 104 | var int a = 0; 105 | var int count = 0; 106 | for (var i = 0; i < pixels.length; i++) { 107 | color = pixels.get(i); 108 | a = Color.alpha(color); 109 | if (a > 0 && notTooBright(color)) { 110 | r += Color.red(color); 111 | g += Color.green(color); 112 | b += Color.blue(color); 113 | count++; 114 | } 115 | } 116 | 117 | if (count == 0){ 118 | // didn't find suitable colours 119 | return defaultColor; 120 | } 121 | 122 | r /= count; 123 | g /= count; 124 | b /= count; 125 | r = (r << 16).bitwiseAnd(0x00FF0000); 126 | g = (g << 8).bitwiseAnd(0x0000FF00); 127 | b = b.bitwiseAnd(0x000000FF); 128 | color = 0xFF000000.bitwiseOr(r).bitwiseOr(g).bitwiseOr(b); 129 | 130 | if (notTooBright(color)) { 131 | return color; 132 | } else { 133 | return defaultColor; 134 | } 135 | } 136 | 137 | def static notTooBright(int color) { 138 | var r = Color.red(color) 139 | var g = Color.green(color) 140 | var b = Color.blue(color) 141 | val threshold = 127 142 | 143 | if (r > threshold && g > threshold && b > threshold) return false; // too bright 144 | 145 | return true 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /app/src/main/res/values-nl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Andere site 4 | Afsluiten 5 | Lettergrootte 6 | Stoppen 7 | Verversen 8 | Afbeeldingen tonen/verbergen 9 | Media delen 10 | Instellingen 11 | Externe domeinen 12 | URL openen 13 | Opslaan als webapp 14 | URL delen 15 | Startscherm-snelkoppeling 16 | Gebruikersagent 17 | Certificaat 18 | 19 | website.nl 20 | URL openen 21 | Ongeldige url 22 | 23 | Externe domeinnamen deblokkeren 24 | Deblokkeren 25 | Webapp opslaan 26 | Wil je de huidige website opslaan als webapp? 27 | Bezig met opslaan 28 | Snelkoppeling toegevoegd 29 | Opslaan 30 | Naam van webapp 31 | Website openen 32 | Aanbevolen 33 | URL delen 34 | Stream delen 35 | Wil je de webapp verwijderen? 36 | Webapp-snelkoppeling 37 | Webapp niet gevonden 38 | Openen met 39 | Media delen 40 | 41 | Websitecertificaat 42 | Niet-vertrouwd certificaat 43 | Certificaat gewijzigd 44 | Certificaat accepteren 45 | 46 | Tips 47 | Webapps toevoegen: voeg een web app toe door de website in kwestie te openen en te drukken op de actie \'Opslaan als webapp\'

49 |

Casten: druk op het castpictogram om media te delen met andere apps op je tv, zoals VLC of Kodi-afstandsbediening (Kore)

50 |

Startscherm-snelkoppelingen: houdt je startscherm lang ingedrukt en ga naar de widgets-pagina om een webapp-snelkoppeling toe te voegen aan je startscherm

51 | ]]>
52 | Broncode 53 | 54 | Afbeeldingen laden ingeschakeld 55 | Afbeeldingen laden uitgeschakeld 56 | Bezig met openen van webapp... 57 | Openen in nieuwe sandbox 58 | 59 | 60 | Externe verzoeken blokkeren 61 | Laad geen afbeeldingen en scripts van buiten webapp-domeinen 62 | Lettergrootte 63 | Pas de lettergrootte aan 64 | Gebruikersagent 65 | Bepaalt hoe websites moeten worden getoond 66 | Volledig scherm 67 | Verberg de statusbalk (herstart vereist) 68 | Beeldvullend 69 | Verberg alle systeemcomponenten in de beeldvullende modus 70 | Actiebalk automatisch verbergen 71 | Verberg de actiebalk als je omlaag scrollt en toon deze weer als je omhoog scrollt 72 | Actiebalk verbergen 73 | Verberg de actiebalk volledig - houdt een leeg gebied lang ingedrukt om in en uit te schakelen 74 | Alleen verbergen in snelkoppelingen 75 | Verberg de actiebalk alleen indien gestart middels een snelkoppeling 76 |
77 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/webviewclient/WebViewUtilsApi19.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.webviewclient 2 | 3 | import android.annotation.TargetApi 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.util.Log 7 | import android.webkit.CookieManager 8 | import android.webkit.WebView 9 | import com.tobykurien.webmediashare.data.Webapp 10 | import java.io.File 11 | 12 | import static extension com.tobykurien.webmediashare.utils.Dependencies.* 13 | 14 | /** 15 | * In API 19+, many things changed with the Webview, rendering previous sandboxing useless. 16 | * This class implements a new strategy for sandboxing. 17 | */ 18 | @TargetApi(19) 19 | class WebViewUtilsApi19 extends WebViewUtilsApi16 { 20 | val static CACHE_DIR = "/org.chromium.android_webview" // where webview stores cache data (inside cache dir) 21 | val static WEBAPP_DIR = "/app_webview" // where webview stores cookies, etc. (inside app's root directory) 22 | 23 | override setupWebView(Context context, WebView wv, Uri siteUrl, Webapp webapp, int defaultFontSize) { 24 | // set up the webview 25 | super.setupWebView(context, wv, siteUrl, webapp, defaultFontSize) 26 | 27 | wv.settings.setMediaPlaybackRequiresUserGesture(false) 28 | 29 | if (false) { 30 | // save previously-viewed webapp's data 31 | saveWebappData(context) 32 | 33 | // clear all caches 34 | wv.clearCache(true) 35 | wv.clearFormData 36 | wv.clearHistory 37 | var cookieManager = CookieManager.getInstance(); 38 | cookieManager.removeAllCookie(); 39 | trimCache(context) 40 | 41 | // restore data for the current webapp, if any 42 | restoreWebappData(context, webapp) 43 | } 44 | } 45 | 46 | // Restore the webapp cache and webview data for sandboxing 47 | def restoreWebappData(Context context, Webapp webapp) { 48 | if (webapp == null || webapp.id < 0) { 49 | context.settings.lastWebappId = -1 50 | return 51 | } 52 | 53 | var appDataDir = WEBAPP_DIR + "_" + webapp.id 54 | var f = new File(context.appDir + appDataDir) 55 | if (f.exists) { 56 | f.renameTo(new File(context.appDir + WEBAPP_DIR)) 57 | var cache = new File(context.appDir + WEBAPP_DIR + CACHE_DIR) 58 | if (cache.exists) { 59 | cache.renameTo(new File(context.cacheDir.absolutePath + CACHE_DIR)) 60 | } 61 | } 62 | 63 | // write the webapp id into a file for saveWebappData to use 64 | context.settings.lastWebappId = webapp.id 65 | } 66 | 67 | // Save the webapp cache and webview data for sandboxing 68 | def saveWebappData(Context context) { 69 | // figure out the last webapp id 70 | var webappId = context.settings.lastWebappId 71 | if (webappId < 0) return 72 | 73 | // save the webview data 74 | var appDataDir = WEBAPP_DIR + "_" + webappId 75 | var f = new File(context.appDir + appDataDir) 76 | if (f.exists) deleteDir(f) // how did that happen? 77 | var dataDir = new File(context.appDir + WEBAPP_DIR) 78 | if (dataDir.exists) { 79 | dataDir.renameTo(f) 80 | 81 | // also save cache data 82 | var cacheDir = new File(context.cacheDir.absolutePath + CACHE_DIR) 83 | if (cacheDir.exists) { 84 | cacheDir.renameTo(new File(context.appDir + appDataDir + CACHE_DIR)) 85 | } 86 | } 87 | } 88 | 89 | override deleteWebappData(Context context, long webappId) { 90 | var webapp = context.db.findById("webapps", webappId, Webapp) 91 | if (webapp !== null) { 92 | var hostname = WebClient.getHost(webapp.url) 93 | CookieManager.instance.setCookie(webapp.url, "") 94 | } 95 | 96 | super.deleteWebappData(context, webappId) 97 | 98 | // delete the saved webview data 99 | var appDataDir = WEBAPP_DIR + "_" + webappId 100 | var f = new File(context.appDir + appDataDir) 101 | if (f.exists) { 102 | deleteDir(f) 103 | } 104 | 105 | if (context.settings.lastWebappId == webappId) { 106 | // delete the last viewed data 107 | f = new File(context.appDir + WEBAPP_DIR) 108 | if (f.exists) { 109 | deleteDir(f) 110 | } 111 | context.settings.lastWebappId == -1 112 | } 113 | } 114 | 115 | def getAppDir(Context context) { 116 | context.filesDir.parent 117 | } 118 | 119 | def private static boolean deleteDir(File dir) { 120 | if (dir != null && dir.isDirectory()) { 121 | var children = dir.list(); 122 | for (String aChildren : children) { 123 | var success = deleteDir(new File(dir, aChildren)); 124 | if (!success) { 125 | return false; 126 | } 127 | } 128 | } 129 | // The directory is now empty so delete it 130 | return dir != null && dir.delete(); 131 | 132 | } 133 | 134 | def void trimCache(Context context) { 135 | try { 136 | var pathadmob = context.appDir + "/" + WEBAPP_DIR; 137 | var dir = new File(pathadmob); 138 | if (dir.isDirectory()) { 139 | deleteDir(dir); 140 | } 141 | 142 | pathadmob = context.cacheDir.absolutePath + "/" + CACHE_DIR; 143 | dir = new File(pathadmob); 144 | if (dir.isDirectory()) { 145 | deleteDir(dir); 146 | } 147 | } catch (Exception e) { 148 | Log.e("webviewutils", "Error deleting cache directories", e) 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # This is a configuration file for ProGuard. 17 | # http://proguard.sourceforge.net/index.html#manual/usage.html 18 | -dontusemixedcaseclassnames 19 | -dontskipnonpubliclibraryclasses 20 | -verbose 21 | # Optimization is turned off by default. Dex does not like code run 22 | # through the ProGuard optimize and preverify steps (and performs some 23 | # of these optimizations on its own). 24 | -dontoptimize 25 | -dontpreverify 26 | 27 | # If you want to enable optimization, you should include the 28 | # following: 29 | # -optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* 30 | # -optimizationpasses 5 31 | # -allowaccessmodification 32 | # 33 | # Note that you cannot just include these flags in your own 34 | # configuration file; if you are including this file, optimization 35 | # will be turned off. You'll need to either edit this file, or 36 | # duplicate the contents of this file and remove the include of this 37 | # file from your project's proguard.config path property. 38 | -keepattributes *Annotation* 39 | -keep public class * extends android.app.Activity 40 | -keep public class * extends android.app.Application 41 | -keep public class * extends android.app.Service 42 | -keep public class * extends android.content.BroadcastReceiver 43 | -keep public class * extends android.content.ContentProvider 44 | -keep public class * extends android.app.backup.BackupAgent 45 | -keep public class * extends android.preference.Preference 46 | -keep public class * extends android.support.v4.app.Fragment 47 | -keep public class * extends android.app.Fragment 48 | # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native 49 | -keepclasseswithmembernames class * { 50 | native ; 51 | } 52 | -keep public class * extends android.view.View { 53 | public (android.content.Context); 54 | public (android.content.Context, android.util.AttributeSet); 55 | public (android.content.Context, android.util.AttributeSet, int); 56 | public void set*(...); 57 | } 58 | -keepclasseswithmembers class * { 59 | public (android.content.Context, android.util.AttributeSet); 60 | } 61 | -keepclasseswithmembers class * { 62 | public (android.content.Context, android.util.AttributeSet, int); 63 | } 64 | -keepclassmembers class * extends android.app.Activity { 65 | public void *(android.view.View); 66 | } 67 | # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations 68 | -keepclassmembers enum * { 69 | public static **[] values(); 70 | public static ** valueOf(java.lang.String); 71 | } 72 | -keep class * implements android.os.Parcelable { 73 | public static final android.os.Parcelable$Creator *; 74 | } 75 | -keepclassmembers class **.R$* { 76 | public static ; 77 | } 78 | # The support library contains references to newer platform versions. 79 | # Don't warn about those in case this app is linking against an older 80 | # platform version. We know about them, and they are safe. 81 | -dontwarn android.support.** 82 | # Needed by google-http-client to keep generic types and @Key annotations accessed via reflection 83 | -keepclassmembers class * { 84 | @com.google.api.client.util.Key ; 85 | } 86 | # Needed just to be safe in terms of keeping Google API service model classes 87 | -keep class com.google.api.services.*.model.* 88 | -keep class com.google.api.client.** 89 | -keepattributes Signature,RuntimeVisibleAnnotations,AnnotationDefault 90 | # See https://groups.google.com/forum/#!topic/guava-discuss/YCZzeCiIVoI 91 | -dontwarn com.google.common.collect.MinMaxPriorityQueue 92 | -dontobfuscate 93 | # Assume dependency libraries Just Work(TM) 94 | -dontwarn com.google.android.youtube.** 95 | -dontwarn com.google.android.analytics.** 96 | -dontwarn com.google.common.** 97 | # Don't discard Guava classes that raise warnings 98 | -keep class com.google.common.collect.MapMakerInternalMap$ReferenceEntry 99 | -keep class com.google.common.cache.LocalCache$ReferenceEntry 100 | # Make sure that Google Analytics doesn't get removed 101 | -keep class com.google.analytics.tracking.android.CampaignTrackingReceiver 102 | ## BEGIN -- Google Play Services proguard.txt 103 | -keep class * extends java.util.ListResourceBundle { 104 | protected java.lang.Object[][] getContents(); 105 | } 106 | # Keep SafeParcelable value, needed for reflection. This is required to support backwards 107 | # compatibility of some classes. 108 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { 109 | public static final *** NULL; 110 | } 111 | # Keep the names of classes/members we need for client functionality. 112 | -keepnames @com.google.android.gms.common.annotation.KeepName class * 113 | -keepclassmembernames class * { 114 | @com.google.android.gms.common.annotation.KeepName *; 115 | } 116 | # Needed for Parcelable/SafeParcelable Creators to not get stripped 117 | -keepnames class * implements android.os.Parcelable { 118 | public static final ** CREATOR; 119 | } 120 | ## END -- Google Play Services proguard.txt 121 | # Other settings 122 | -keep class com.android.** 123 | -keep class com.google.android.** 124 | -keep class com.google.android.gms.** 125 | -keep class com.google.android.gms.location.** 126 | -keep class com.google.api.client.** 127 | -keep class com.google.maps.android.** 128 | -keep class libcore.** 129 | 130 | -dontwarn javax.annotation.** 131 | -dontwarn javax.inject.** 132 | -dontwarn sun.misc.Unsafe 133 | -dontwarn java.beans.** 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/activity/MainActivity.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.activity 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.support.v7.app.AlertDialog 9 | import android.support.v7.app.AppCompatActivity 10 | import android.text.Html 11 | import android.view.Menu 12 | import android.view.MenuItem 13 | import android.view.View 14 | import android.view.WindowManager 15 | import android.webkit.CookieManager 16 | import com.tobykurien.webmediashare.R 17 | import com.tobykurien.webmediashare.adapter.WebappsAdapter 18 | import com.tobykurien.webmediashare.data.Webapp 19 | import com.tobykurien.webmediashare.db.DbService 20 | import com.tobykurien.webmediashare.fragment.DlgOpenUrl 21 | import com.tobykurien.webmediashare.utils.FaviconHandler 22 | import com.tobykurien.webmediashare.webviewclient.WebViewUtils 23 | import java.util.List 24 | import org.xtendroid.app.AndroidActivity 25 | import org.xtendroid.app.OnCreate 26 | import org.xtendroid.utils.AsyncBuilder 27 | 28 | import static extension com.tobykurien.webmediashare.utils.Dependencies.* 29 | import static extension org.xtendroid.utils.AlertUtils.* 30 | import com.tobykurien.webmediashare.webviewclient.WebClient 31 | 32 | @AndroidActivity(R.layout.main) class MainActivity extends AppCompatActivity { 33 | var protected List webapps 34 | 35 | @OnCreate 36 | def init(Bundle savedInstanceState) { 37 | if(settings.isFullscreen()) { 38 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 39 | WindowManager.LayoutParams.FLAG_FULLSCREEN); 40 | } 41 | 42 | if (intent != null && intent.getDataString() != null) { 43 | DlgOpenUrl.openUrl(this, intent.getDataString(), false) 44 | } else if (intent != null && intent.getStringExtra(Intent.EXTRA_TEXT) != null) { 45 | DlgOpenUrl.openUrl(this, intent.getStringExtra(Intent.EXTRA_TEXT), false) 46 | } 47 | } 48 | 49 | override protected onStart() { 50 | super.onStart() 51 | 52 | val activity = this 53 | loadWebapps() 54 | 55 | mainList.setOnItemClickListener([av, v, pos, id| 56 | val item = av.getItemAtPosition(pos) as Webapp 57 | var intent = new Intent(activity, typeof(WebAppActivity)) 58 | intent.action = Intent.ACTION_VIEW 59 | intent.data = Uri.parse(webapps.get(pos).url) 60 | BaseWebAppActivity.putWebappId(intent, item.id) 61 | BaseWebAppActivity.putFromShortcut(intent, false) 62 | startActivity(intent) 63 | ]) 64 | 65 | mainList.setOnItemLongClickListener([av, v, pos, id| 66 | val item = av.getItemAtPosition(pos) as Webapp 67 | confirm(getString(R.string.delete_webapp), [ 68 | AsyncBuilder.async[p1, p2| 69 | db.execute(R.string.dbDeleteDomains, # {'webappId' -> item.id}) 70 | db.delete(DbService.TABLE_WEBAPPS, String.valueOf(item.id)) 71 | new FaviconHandler(this).deleteFavIcon(item.id) 72 | WebViewUtils.instance.deleteWebappData(this, item.id) 73 | null 74 | ].then [ 75 | loadWebapps 76 | ].start 77 | ]) 78 | true 79 | ]) 80 | 81 | // show tips on first load 82 | if(settings.firstLoaded < 1) { 83 | settings.firstLoaded = 1 84 | showTips() 85 | } 86 | } 87 | 88 | override onResume() { 89 | super.onResume() 90 | handleFullscreenOptions(this) 91 | } 92 | 93 | 94 | override onCreateOptionsMenu(Menu menu) { 95 | menuInflater.inflate(R.menu.main_menu, menu) 96 | true 97 | } 98 | 99 | override onOptionsItemSelected(MenuItem item) { 100 | switch (item.itemId) { 101 | case R.id.menu_open: { 102 | var dlg = new DlgOpenUrl() 103 | dlg.show(supportFragmentManager, "open_url") 104 | } 105 | 106 | case R.id.menu_tips: { 107 | showTips() 108 | } 109 | 110 | case R.id.menu_settings: { 111 | var i = new Intent(this, Preferences) 112 | startActivity(i) 113 | } 114 | 115 | case R.id.menu_exit: finish() 116 | } 117 | super.onOptionsItemSelected(item) 118 | } 119 | 120 | def showTips() { 121 | new AlertDialog.Builder(this) 122 | .setTitle(R.string.action_tips) 123 | .setMessage(Html.fromHtml(getString(R.string.tips))) 124 | .setPositiveButton(android.R.string.ok, null) 125 | .setNeutralButton(R.string.btn_website, [ 126 | val link = Uri.parse("https://github.com/tobykurien/WebMediaShare") 127 | WebClient.handleExternalLink(this, link, true); 128 | ]) 129 | .create() 130 | .show() 131 | } 132 | 133 | def loadWebapps() { 134 | webapps = db.getWebapps() 135 | var adapter = new WebappsAdapter(this, webapps) 136 | mainList.setAdapter(adapter) 137 | 138 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 139 | try { 140 | if (!settings.cookiesImported && webapps !== null) { 141 | // import old cookies from WebView into our new db storage 142 | for (webapp: webapps) { 143 | db.saveCookies(webapp) 144 | } 145 | 146 | settings.cookiesImported = true 147 | 148 | // now we can delete all cookies from WebView 149 | CookieManager.instance.removeAllCookie() 150 | } 151 | } catch (Exception e) { 152 | toast("Error importing old cookies " + e.class.name + " - " + e.message) 153 | } 154 | } 155 | } 156 | 157 | def static handleFullscreenOptions(Activity activity) { 158 | if(activity.settings.isFullscreen()) { 159 | val decorView = activity.getWindow().getDecorView(); 160 | if(activity.settings.isFullscreenImmersive()) { 161 | decorView.setSystemUiVisibility( 162 | View.SYSTEM_UI_FLAG_IMMERSIVE 163 | .bitwiseOr(View.SYSTEM_UI_FLAG_FULLSCREEN) 164 | .bitwiseOr(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) 165 | ); 166 | } else { 167 | decorView.setSystemUiVisibility( 168 | View.SYSTEM_UI_FLAG_FULLSCREEN 169 | ); 170 | } 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/dlg_certificate_changed.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 13 | 14 | 18 | 19 | 25 | 26 | 32 | 33 | 34 | 38 | 39 | 43 | 44 | 48 | 49 | 55 | 56 | 60 | 61 | 62 | 66 | 67 | 72 | 73 | 77 | 78 | 79 | 83 | 84 | 89 | 90 | 94 | 95 | 96 | 97 | 101 | 102 | 106 | 107 | 113 | 114 | 118 | 119 | 120 | 124 | 125 | 130 | 131 | 135 | 136 | 137 | 141 | 142 | 147 | 148 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 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 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/activity/BaseWebAppActivity.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.activity 2 | 3 | import android.annotation.TargetApi 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.Bitmap 7 | import android.net.Uri 8 | import android.os.Bundle 9 | import android.provider.MediaStore 10 | import android.support.v7.app.AppCompatActivity 11 | import android.util.Log 12 | import android.view.KeyEvent 13 | import android.view.View 14 | import android.view.WindowManager 15 | import android.webkit.ClientCertRequest 16 | import android.webkit.CookieManager 17 | import android.webkit.CookieSyncManager 18 | import android.webkit.ValueCallback 19 | import android.webkit.WebChromeClient 20 | import android.webkit.WebChromeClient.FileChooserParams 21 | import android.webkit.WebView 22 | import android.widget.ProgressBar 23 | import com.tobykurien.webmediashare.R 24 | import com.tobykurien.webmediashare.data.Webapp 25 | import com.tobykurien.webmediashare.db.DbService 26 | import com.tobykurien.webmediashare.utils.Debug 27 | import com.tobykurien.webmediashare.webviewclient.WebClient 28 | import com.tobykurien.webmediashare.webviewclient.WebViewUtils 29 | import java.io.File 30 | import java.io.IOException 31 | import java.text.SimpleDateFormat 32 | import java.util.Date 33 | import java.util.HashSet 34 | import java.util.Set 35 | import org.xtendroid.annotations.BundleProperty 36 | import org.xtendroid.app.AndroidActivity 37 | import org.xtendroid.app.OnCreate 38 | 39 | import static extension com.tobykurien.webmediashare.utils.Dependencies.* 40 | import static extension org.xtendroid.utils.AlertUtils.* 41 | 42 | @TargetApi(21) 43 | @AndroidActivity(R.layout.webapp) class BaseWebAppActivity extends AppCompatActivity { 44 | // Required intent arguments 45 | @BundleProperty package long webappId = -1 46 | @BundleProperty boolean fromShortcut = true // launched from shortcut? 47 | 48 | public static boolean reload = false 49 | package WebView wv = null 50 | package Uri siteUrl = null 51 | package WebClient wc = null 52 | package Webapp webapp = null 53 | package Set unblock = new HashSet 54 | 55 | val static int FILECHOOSER_RESULTCODE = 101 56 | val static int REQUEST_SELECT_FILE = 102 57 | private ValueCallback mUploadMessage; 58 | private ValueCallback mUploadMessage2; 59 | private String mCameraPhotoPath; 60 | 61 | /** 62 | * Called when the activity is first created. 63 | */ 64 | @OnCreate 65 | def void init(Bundle savedInstanceState) { 66 | wv = siteWebview 67 | if (wv === null) { 68 | finish() 69 | return; 70 | } 71 | 72 | siteUrl = intent?.data 73 | if (siteUrl == null) return; 74 | 75 | if (webappId >= 0) { 76 | webapp = db.findById(DbService.TABLE_WEBAPPS, webappId, Webapp) 77 | if (webapp == null) { 78 | toast(getString(R.string.err_webapp_not_found)) 79 | finish() 80 | return; 81 | } 82 | } else { 83 | webapp = new Webapp() 84 | webapp.url = siteUrl.toString 85 | webapp.name = webapp.url 86 | putFromShortcut(false) 87 | } 88 | 89 | val pb = siteProgress 90 | if(pb !== null) pb.setVisibility(View.VISIBLE) 91 | 92 | setupWebView() 93 | wv.setWebViewClient(getWebViewClient(pb)) 94 | 95 | // save the favicon for later use if we get one 96 | wv.setWebChromeClient(new WebChromeClient() { 97 | override void onReceivedIcon(WebView view, Bitmap icon) { 98 | super.onReceivedIcon(view, icon) 99 | onReceivedFavicon(view, icon) 100 | } 101 | 102 | // openFileChooser for Android < 3.0 103 | def void openFileChooser(ValueCallback uploadMsg) { 104 | openFileChooser(uploadMsg, ""); 105 | } 106 | 107 | // openFileChooser for other Android versions 108 | def void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) { 109 | openFileChooser(uploadMsg, acceptType); 110 | } 111 | 112 | override onShowFileChooser(WebView webView, ValueCallback filePathCallback, 113 | WebChromeClient.FileChooserParams fileChooserParams) { 114 | openFileChooserLollipop(filePathCallback, fileChooserParams) 115 | } 116 | 117 | override onShowCustomView(View view, CustomViewCallback callback) { 118 | super.onShowCustomView(view, callback) 119 | fullscreenView.addView(view) 120 | onFullscreenChanged(true) 121 | } 122 | 123 | override onHideCustomView() { 124 | super.onHideCustomView() 125 | onFullscreenChanged(false) 126 | } 127 | 128 | 129 | }) 130 | 131 | openSite(webapp, siteUrl) 132 | } 133 | 134 | def protected void setupWebView() { 135 | WebViewUtils.getInstance().setupWebView(this, wv, siteUrl, webapp, 136 | settings.getIntFontSize()) 137 | } 138 | 139 | override protected void onResume() { 140 | super.onResume() 141 | CookieSyncManager.getInstance().startSync() 142 | if (reload) { 143 | reload = false 144 | setupWebView() 145 | } 146 | 147 | } 148 | 149 | override protected void onPause() { 150 | super.onPause() 151 | CookieSyncManager.getInstance().stopSync() 152 | } 153 | 154 | def void onReceivedFavicon(WebView view, Bitmap icon) { 155 | } 156 | 157 | def void onPageLoadStarted() { 158 | } 159 | 160 | def void onPageLoadDone() { 161 | } 162 | 163 | def onFullscreenChanged(boolean isFullscreen) { 164 | setFullscreen(isFullscreen) 165 | 166 | if (isFullscreen) { 167 | wv.visibility = View.GONE 168 | fullscreenView.visibility = View.VISIBLE 169 | } else { 170 | wv.visibility = View.VISIBLE 171 | fullscreenView.visibility = View.GONE 172 | } 173 | } 174 | 175 | def void onClientCertificateRequest(ClientCertRequest request) { 176 | } 177 | 178 | /** 179 | * Return the web view client for the web view 180 | * @param pb 181 | * @return 182 | */ 183 | def protected WebClient getWebViewClient(ProgressBar pb) { 184 | if (wc === null) { 185 | unblock = new HashSet() 186 | unblock.add(WebClient.getHost(siteUrl)) 187 | if (webappId >= 0) { 188 | // load saved unblock list 189 | var domains = db.executeForMapList(R.string.dbGetDomainNames, #{ 190 | "webappId" -> webappId 191 | }) 192 | for (domain : domains) { 193 | unblock.add(domain.get("domain") as String) 194 | } 195 | } 196 | wc = new WebClient(this, webapp, wv, pb, unblock) 197 | } 198 | return wc 199 | } 200 | 201 | def void openSite(Webapp webapp, Uri siteUrl) { 202 | // TODO - use okHttp to check the site cert before connecting 203 | 204 | // Request request = new Request.Builder() 205 | // .url(url) 206 | // .build(); 207 | 208 | // Response response = client.newCall(request).execute(); 209 | // if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 210 | 211 | // for (Certificate certificate : response.handshake().peerCertificates()) { 212 | // System.out.println(CertificatePinner.pin(certificate)); 213 | // } 214 | 215 | // Load cookies for webapp 216 | CookieManager.instance.removeAllCookie() 217 | if (webapp.cookies !== null) { 218 | val domain = WebClient.getRootDomain(webapp.url) 219 | var cookies = webapp.cookies.split(";") 220 | for (cookieStr: cookies) { 221 | if (Debug.COOKIE) Log.d("cookie", "Loading cookie for " + domain + ": " + cookieStr) 222 | CookieManager.instance.setCookie(domain, cookieStr.trim() + "; Domain=" + domain) 223 | } 224 | CookieSyncManager.getInstance().sync(); 225 | } 226 | 227 | var url = siteUrl.toString() 228 | wv.loadUrl(url) 229 | } 230 | 231 | override boolean onKeyDown(int keyCode, KeyEvent event) { 232 | if ((keyCode === KeyEvent.KEYCODE_BACK) && wv.canGoBack()) { 233 | wv.goBack() 234 | return true 235 | } 236 | return super.onKeyDown(keyCode, event) 237 | } 238 | 239 | def openFileChooser(ValueCallback uploadMsg, String acceptType) { 240 | Log.i("WebChromeClient", "openFileChooser() called."); 241 | 242 | if(mUploadMessage != null) mUploadMessage.onReceiveValue(null); 243 | mUploadMessage = uploadMsg; 244 | 245 | val intent = new Intent(Intent.ACTION_GET_CONTENT); 246 | intent.addCategory(Intent.CATEGORY_OPENABLE); 247 | intent.setType("*/*"); 248 | startActivityForResult(Intent.createChooser(intent, "File Chooser"), FILECHOOSER_RESULTCODE); 249 | 250 | return true; 251 | } 252 | 253 | def openFileChooserLollipop(ValueCallback filePathCallback, FileChooserParams fileChooserParams) { 254 | Log.i("WebChromeClient", "openFileChooserLollipop() called."); 255 | if (mUploadMessage2 != null) { 256 | mUploadMessage2.onReceiveValue(null); 257 | } 258 | mUploadMessage2 = filePathCallback; 259 | 260 | var takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 261 | if (takePictureIntent.resolveActivity(this.getPackageManager()) != null) { 262 | // Create the File where the photo should go 263 | var File photoFile = null; 264 | try { 265 | photoFile = createImageFile(); 266 | takePictureIntent.putExtra("PhotoPath", photoFile.absolutePath); 267 | } catch (IOException ex) { 268 | // Error occurred while creating the File 269 | Log.e("base webapp activity", "Unable to create Image File", ex); 270 | } 271 | 272 | // Continue only if the File was successfully created 273 | if (photoFile != null) { 274 | mCameraPhotoPath = "file:" + photoFile.getAbsolutePath(); 275 | takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile)); 276 | } else { 277 | takePictureIntent = null; 278 | } 279 | } 280 | 281 | var contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); 282 | contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); 283 | contentSelectionIntent.setType("*/*"); 284 | 285 | var Intent[] intentArray; 286 | if (takePictureIntent != null) { 287 | intentArray = newArrayList(takePictureIntent); 288 | } else { 289 | intentArray = newArrayList() 290 | } 291 | 292 | var chooserIntent = new Intent(Intent.ACTION_CHOOSER); 293 | chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); 294 | chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser"); 295 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); 296 | startActivityForResult(chooserIntent, REQUEST_SELECT_FILE); 297 | 298 | return true 299 | } 300 | 301 | override protected onActivityResult(int requestCode, int resultCode, Intent intent) { 302 | try { 303 | if (requestCode == FILECHOOSER_RESULTCODE) { 304 | if(null == mUploadMessage) return; 305 | var result = if(intent == null || resultCode != RESULT_OK) null else intent.getData() 306 | mUploadMessage.onReceiveValue(result); 307 | mUploadMessage = null; 308 | } else if (requestCode == REQUEST_SELECT_FILE) { 309 | // Check that the response is a good one 310 | var Uri[] results = null; 311 | if (resultCode == RESULT_OK) { 312 | if (intent == null || intent.getDataString() == null) { 313 | // If there is not data, then we may have taken a photo 314 | if (mCameraPhotoPath != null) { 315 | results = #[Uri.parse(mCameraPhotoPath)]; 316 | } 317 | } else { 318 | var String dataString = intent.getDataString(); 319 | if (dataString != null) { 320 | val uri = Uri.parse(dataString); 321 | results = #[uri]; 322 | 323 | try { 324 | // as per https://developer.android.com/guide/topics/providers/document-provider.html#permissions 325 | val int takeFlags = intent.getFlags().bitwiseAnd(Intent.FLAG_GRANT_READ_URI_PERMISSION) 326 | // Check for the freshest data. 327 | getContentResolver().takePersistableUriPermission(uri, takeFlags); 328 | } catch (Exception e) { 329 | // couldn't get persistable permissions, aaah well. 330 | Log.e("upload", "error taking persistable permission", e) 331 | } 332 | } 333 | } 334 | } 335 | 336 | mUploadMessage2.onReceiveValue(results); 337 | mUploadMessage2 = null; 338 | mCameraPhotoPath = null 339 | } else { 340 | super.onActivityResult(requestCode, resultCode, intent) 341 | } 342 | } catch (Exception e) { 343 | toastLong("Unable to process: " + e.class.simpleName + " " + e.message) 344 | } 345 | } 346 | 347 | def static File createImageFile(Context context) throws IOException { 348 | // Create an image file name 349 | var String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); 350 | var String imageFileName = "JPEG_" + timeStamp + '_'; 351 | var File storageDir = context.cacheDir; 352 | return File.createTempFile( 353 | imageFileName, /* prefix */ 354 | ".jpg", /* suffix */ 355 | storageDir /* directory */ 356 | ); 357 | } 358 | 359 | def void setFullscreen(boolean fullscreen) { 360 | var attrs = getWindow().getAttributes(); 361 | 362 | if (fullscreen) { 363 | attrs.flags = attrs.flags.bitwiseOr(WindowManager.LayoutParams.FLAG_FULLSCREEN); 364 | } else { 365 | attrs.flags = attrs.flags.bitwiseAnd(WindowManager.LayoutParams.FLAG_FULLSCREEN.bitwiseNot); 366 | } 367 | 368 | getWindow().setAttributes(attrs); 369 | } 370 | } 371 | 372 | -------------------------------------------------------------------------------- /app/src/main/java/com/tobykurien/webmediashare/webviewclient/WebClient.xtend: -------------------------------------------------------------------------------- 1 | package com.tobykurien.webmediashare.webviewclient 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.net.Uri 7 | import android.net.http.SslError 8 | import android.support.v4.content.LocalBroadcastManager 9 | import android.support.v7.app.AlertDialog 10 | import android.util.Log 11 | import android.view.View 12 | import android.webkit.ClientCertRequest 13 | import android.webkit.CookieManager 14 | import android.webkit.CookieSyncManager 15 | import android.webkit.SslErrorHandler 16 | import android.webkit.WebResourceResponse 17 | import android.webkit.WebView 18 | import android.webkit.WebViewClient 19 | import android.widget.Toast 20 | import com.tobykurien.webmediashare.R 21 | import com.tobykurien.webmediashare.activity.BaseWebAppActivity 22 | import com.tobykurien.webmediashare.activity.WebAppActivity 23 | import com.tobykurien.webmediashare.data.MediaUrl 24 | import com.tobykurien.webmediashare.data.Webapp 25 | import com.tobykurien.webmediashare.fragment.DlgCertificate 26 | import com.tobykurien.webmediashare.utils.Debug 27 | import java.io.ByteArrayInputStream 28 | import java.io.File 29 | import java.io.FileReader 30 | import java.net.HttpURLConnection 31 | import java.net.URL 32 | import java.util.ArrayList 33 | import java.util.HashMap 34 | import java.util.List 35 | import java.util.Set 36 | import java.io.BufferedReader 37 | 38 | import static org.xtendroid.utils.AsyncBuilder.* 39 | import static extension com.tobykurien.webmediashare.utils.Dependencies.* 40 | import static extension org.xtendroid.utils.AlertUtils.* 41 | 42 | class WebClient extends WebViewClient { 43 | public static val UNKNOWN_HOST = "999.999.999.999" // impossible hostname to avoid vuln 44 | public static val MEDIA_URL_FOUND = "com.tobykurien.webmediashare.MEDIA_URL_FOUND" 45 | 46 | package BaseWebAppActivity activity 47 | package Webapp webapp 48 | package WebView wv 49 | package View pd 50 | public Set domainUrls 51 | public Set adblockHosts = newHashSet() 52 | package var blockedHosts = new HashMap() 53 | public var ArrayList mediaUrls = newArrayList() 54 | 55 | new(BaseWebAppActivity activity, Webapp webapp, WebView wv, View pd, Set domainUrls) { 56 | this.activity = activity 57 | this.webapp = webapp 58 | this.wv = wv 59 | this.pd = pd 60 | this.domainUrls = domainUrls 61 | if (adblockHosts.empty) { 62 | loadAdblockHosts() 63 | } 64 | } 65 | 66 | override onReceivedClientCertRequest(WebView view, ClientCertRequest request) { 67 | super.onReceivedClientCertRequest(view, request) 68 | activity.onClientCertificateRequest(request) 69 | } 70 | 71 | override void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { 72 | handler.proceed() 73 | 74 | // if (webapp == null || webapp.certIssuedBy == null) { 75 | // // no SSL cert was saved for this webapp, so show SSL error to user 76 | // var dlg = new DlgCertificate(error.certificate, 77 | // activity.getString(R.string.title_cert_untrusted), 78 | // activity.getString(R.string.cert_accept), [ 79 | // handler.proceed() 80 | // true 81 | // ], [ 82 | // handler.cancel() 83 | // true 84 | // ]) 85 | // dlg.show(activity.supportFragmentManager, "certificate") 86 | // } else { 87 | // // in onPageLoaded, WebAppActivity will check that the cert matches saved one 88 | // handler.proceed() 89 | // } 90 | } 91 | 92 | override void onPageFinished(WebView view, String url) { 93 | if(pd !== null) pd.setVisibility(View.GONE) 94 | activity.onPageLoadDone() 95 | CookieSyncManager.getInstance().sync() 96 | super.onPageFinished(view, url) 97 | } 98 | 99 | override void onPageStarted(WebView view, String url, Bitmap favicon) { 100 | //Log.d("webclient", '''loading «url»''') 101 | if(pd !== null) pd.setVisibility(View.VISIBLE) 102 | activity.onPageLoadStarted() 103 | 104 | mediaUrls.clear() 105 | LocalBroadcastManager.getInstance(wv.context).sendBroadcast(new Intent 106 | (MEDIA_URL_FOUND)) 107 | 108 | super.onPageStarted(view, url, favicon) 109 | } 110 | 111 | override boolean shouldOverrideUrlLoading(WebView view, String url) { 112 | 113 | if (!getRootDomain(url).equals(getRootDomain(webapp.url))) { 114 | try { 115 | handleExternalLink(view.context, Uri.parse(url), false) 116 | } catch (Exception e) { 117 | // probably bad url 118 | Log.e("WebClient", "error handling external url", e) 119 | } 120 | return true 121 | } 122 | 123 | return super.shouldOverrideUrlLoading(view, url) 124 | } 125 | 126 | def synchronized shareUrl(Uri uri, String contentType, Long contentLength) { 127 | for (mu : mediaUrls) { 128 | if (mu.uri.toString().equals(uri.toString)) { 129 | // url already added 130 | return 131 | } 132 | } 133 | 134 | val mu = new MediaUrl() 135 | mu.uri = uri 136 | mu.contentType = contentType 137 | mu.contentLength = contentLength 138 | mediaUrls.add(mu) 139 | 140 | // alert other components that we found a media URL 141 | LocalBroadcastManager.getInstance(wv.context).sendBroadcast(new Intent 142 | (MEDIA_URL_FOUND)) 143 | } 144 | 145 | def static handleExternalLink(Context activity, Uri uri, boolean openInExternalApp) { 146 | handleExternalLink(activity, uri, openInExternalApp, true) 147 | } 148 | 149 | def static handleExternalLink(Context activity, Uri uri, boolean openInExternalApp, 150 | boolean isFromWebapp) { 151 | val domain = getRootDomain(uri.toString()) 152 | Log.d("url_loading", domain) 153 | // first check if we have a saved webapp for this URI 154 | val webapps = activity.db.getWebapps().filter [wa| 155 | // check against root domains rather than sub-domains 156 | getRootDomain(wa.url).equals(getRootDomain(domain)) 157 | ] 158 | 159 | if (webapps == null || webapps.length == 0) { 160 | if (openInExternalApp) { 161 | Log.d("url_loading", "Sending to default app " + uri.toString) 162 | var Intent i = new Intent(Intent.ACTION_VIEW) 163 | i.setData(uri) 164 | activity.startActivity(i) 165 | } else { 166 | if (isFromWebapp) { 167 | // prevent webaps from opening popups or redirecting to other sites 168 | activity.toast(activity.getString(R.string.popup_blocked)) 169 | Log.d("url_loading", "Ignoring URL as it is outside sandbox " + uri.toString) 170 | } else { 171 | // open in new sandbox 172 | // delete all previous cookies 173 | CookieManager.instance.removeAllCookie() 174 | var i = new Intent(activity, WebAppActivity) 175 | i.action = Intent.ACTION_VIEW 176 | i.data = uri 177 | activity.startActivity(i) 178 | } 179 | } 180 | } else { 181 | if (webapps.length > 1) { 182 | Log.d("url_loading", "More than one registered webapp for " + uri.toString) 183 | // TODO ask user to pick a webapp 184 | new AlertDialog.Builder(activity) 185 | .setTitle(R.string.title_open_with) 186 | .setItems(webapps.map[ name ], [a, pos| 187 | openWebapp(activity, webapps.get(pos), uri) 188 | ]) 189 | .setNegativeButton(android.R.string.cancel, [ ]) 190 | .create() 191 | .show() 192 | } else { 193 | Log.d("url_loading", "Opening registered webapp for " + uri.toString) 194 | openWebapp(activity, webapps.get(0), uri) 195 | } 196 | } 197 | } 198 | 199 | override WebResourceResponse shouldInterceptRequest(WebView view, String url) { 200 | // Block 3rd party requests (i.e. scripts/iframes/etc. outside Google's domains) 201 | // and also any unencrypted connections 202 | val Uri uri = Uri.parse(url) 203 | val siteUrl = getHost(uri) 204 | var boolean isBlocked = false 205 | 206 | // block ads 207 | if (!adblockHosts.empty) { 208 | val root = getRootDomain(uri.host) 209 | if (adblockHosts.exists[ it.equals(root) ]) { 210 | isBlocked = true 211 | } 212 | } 213 | 214 | if (isBlocked) { 215 | if (Debug.ON) Log.d("webclient", "Blocking " + url); 216 | //blockedHosts.put(getRootDomain(url), true) 217 | return new WebResourceResponse("text/plain", "utf-8", new ByteArrayInputStream("".getBytes())) 218 | } 219 | 220 | try { 221 | if (uri.path.contains(".")) { 222 | var media = #[ 223 | // playlists 224 | ".m3u8",".m3u",".pls", 225 | // video 226 | ".mp4",".mpv",".mpeg",".webm",".vp9",".ogv",".mkv",".avi",".gifv", 227 | // audio 228 | ".aac", ".ogg", ".mp3", ".m4a", ".nsv" 229 | ].exists[ uri.path.endsWith(it) ] 230 | 231 | if (media) { 232 | Log.d("CAST", "Found media " + url) 233 | shareUrl(uri, "video/mpeg", -1l) 234 | } else { 235 | //Log.d("CAST", "skipping " + uri.toString) 236 | } 237 | } else { 238 | // check the content type for playable media 239 | async() [ builder, params | 240 | var con = new URL(url).openConnection() as HttpURLConnection 241 | //con.setRequestMethod("HEAD") 242 | if (activity.settings.userAgent != null && 243 | activity.settings.userAgent.trim().length > 0) { 244 | // User-agent may affect site redirects 245 | con.setRequestProperty("User-Agent", activity.settings.userAgent) 246 | } 247 | val ret = #[ con.getContentType(), con.getContentLength() as long, con.getURL() ] 248 | try { 249 | con.inputStream.close() 250 | } catch (Exception e) { 251 | // ignore close error 252 | } 253 | return ret 254 | ].then[ List result | 255 | val contentType = result.get(0) as String 256 | val contentLength = result.get(1) as Long 257 | val url2 = result.get(2) as URL 258 | 259 | if (contentType?.startsWith("video/") || contentType?.startsWith("audio/")) { 260 | Log.d("CAST", result.toString() + ": " + url2) 261 | shareUrl(Uri.parse(url2.toString), contentType, contentLength) 262 | } 263 | ].onError[ error | 264 | // ignore errors 265 | Log.e("CAST", "ERROR checking " + url, error) 266 | ].start() 267 | } 268 | } catch (Exception e) { 269 | Log.d("CAST", e.class.simpleName + " " + e.message) 270 | } 271 | 272 | val cookieManager = CookieManager.instance 273 | if (Debug.COOKIE && siteUrl !== null) Log.d("cookie", "Cookies for " + siteUrl + ": " + 274 | cookieManager.getCookie(siteUrl.toString())) 275 | 276 | return super.shouldInterceptRequest(view, url) 277 | } 278 | 279 | // Get the host/domain from a URL or a host string. 280 | def public static String getHost(Uri uri, String defaultHost) { 281 | if (uri === null) return defaultHost 282 | var ret = uri.getHost() 283 | if (ret !== null) { 284 | return ret 285 | } else { 286 | return defaultHost 287 | } 288 | } 289 | 290 | // Get the host/domain from a URL or a host string. 291 | def public static String getHost(String url, String defaultHost) { 292 | if (url == null) return defaultHost 293 | try { 294 | if (url.indexOf("://") > 0) { 295 | return getHost(Uri.parse(url)) 296 | } else { 297 | return getHost(Uri.parse("https://" + url)) 298 | } 299 | } catch (Exception e) { 300 | Log.e("host", "Error parsing " + url, e) 301 | return defaultHost 302 | } 303 | } 304 | 305 | def public static String getHost(Uri uri) { 306 | var ret = getHost(uri, UNKNOWN_HOST) 307 | //Log.d("host", "Uri " + uri.toString() + " -> " + ret) 308 | return ret 309 | } 310 | 311 | def public static String getHost(String url) { 312 | var ret = getHost(url, UNKNOWN_HOST) 313 | //Log.d("host", "Url " + url + " -> " + ret) 314 | return ret 315 | } 316 | 317 | /** 318 | * Most blocked 3rd party domains are CDNs, so rather use root domain 319 | * @param url 320 | * @return 321 | */ 322 | def public static String getRootDomain(String url) { 323 | var String host = getHost(url) 324 | 325 | try { 326 | var String[] parts = host.split("\\.").reverseView() 327 | if (parts.length > 2) { 328 | // handle things like mobile.site.co.za vs www1.api.site.com 329 | if (parts.get(0).length == 2 && parts.get(1).length <= 3) { 330 | return '''«{parts.get(2)}».«{parts.get(1)}».«{parts.get(0)}»''' 331 | } else { 332 | return '''«{parts.get(1)}».«{parts.get(0)}»''' 333 | } 334 | } else if (parts.length > 1) { 335 | return '''«{parts.get(1)}».«{parts.get(0)}»''' 336 | } else { 337 | return host 338 | } 339 | } catch (Exception e) { 340 | // sometimes things don't quite work out 341 | return host 342 | } 343 | } 344 | 345 | override void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { 346 | super.onReceivedError(view, errorCode, description, failingUrl) 347 | Toast.makeText(activity, description, Toast.LENGTH_LONG).show() 348 | } 349 | 350 | def void openWebapp(Webapp webapp, Uri uri) { 351 | openWebapp(activity, webapp, uri) 352 | } 353 | 354 | def static void openWebapp(Context activity, Webapp webapp, Uri uri) { 355 | var intent = new Intent(activity, typeof(WebAppActivity)) 356 | intent.action = Intent.ACTION_VIEW 357 | intent.data = Uri.parse(uri.toString) 358 | BaseWebAppActivity.putWebappId(intent, webapp.id) 359 | BaseWebAppActivity.putFromShortcut(intent, false) 360 | activity.startActivity(intent) 361 | } 362 | 363 | /** 364 | * Parse the Uri and return an actual Uri to load. This will handle 365 | * exceptions, like loading a URL 366 | * that is passed in the "url" parameter, to bypass click-throughs, etc. 367 | * @param uri 368 | * @return 369 | */ 370 | def protected Uri getLoadUri(Uri uri) { 371 | if(uri === null) return uri // handle google news links to external sites directly 372 | try { 373 | if (uri.getQueryParameter("url") !== null) { 374 | return Uri.parse(uri.getQueryParameter("url")) 375 | } 376 | } catch (UnsupportedOperationException e) { 377 | // Not a hierarchical uri with a query parameter, like data: 378 | return uri 379 | } 380 | return uri 381 | } 382 | 383 | /** 384 | * Returns true if the linked site is within the Webapp's domain 385 | * @param uri 386 | * @return 387 | */ 388 | def public static boolean isInSandbox(Uri uri, Set domainUrls) { 389 | if("data".equals(uri.getScheme()) || "blob".equals(uri.getScheme())) return true 390 | var String host = uri.getHost() 391 | if (host == null) return true; 392 | 393 | for (String sites : domainUrls) { 394 | for (String site : sites.split(" ")) { 395 | if (site != null && host.toLowerCase().endsWith(site.toLowerCase())) { 396 | return true 397 | } 398 | 399 | } 400 | 401 | } 402 | return false 403 | } 404 | 405 | def protected boolean isInSandbox(Uri uri) { 406 | return isInSandbox(uri, domainUrls) 407 | } 408 | 409 | def Set getBlockedHosts() { 410 | blockedHosts.keySet() 411 | } 412 | 413 | /** 414 | * Add domains to be unblocked 415 | * @param unblock 416 | */ 417 | def void unblockDomains(Set unblock) { 418 | for (String s : domainUrls) { 419 | unblock.add(s) 420 | } 421 | domainUrls = unblock 422 | } 423 | 424 | def loadAdblockHosts() { 425 | if (adblockHosts.empty) { 426 | val adhosts = new File(wv.context.getCacheDir().absolutePath + "/adhosts") 427 | if (adhosts.exists && adhosts.canRead) { 428 | val fis = new BufferedReader(new FileReader(adhosts)) 429 | try { 430 | var String line; 431 | while ((line = fis.readLine) != null) { 432 | adblockHosts.add(line) 433 | } 434 | } catch (Exception e) { 435 | Log.e("adblock", "Unable to read adblock list", e) 436 | } finally { 437 | fis.close() 438 | } 439 | } 440 | } 441 | } 442 | } 443 | --------------------------------------------------------------------------------