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 |
7 |
8 |
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 |
--------------------------------------------------------------------------------
/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 |
4 |
5 |
6 |
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 |   
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 |
--------------------------------------------------------------------------------
/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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | generateDebugSources
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
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 |
--------------------------------------------------------------------------------