├── .editorconfig ├── LICENSE ├── Migration.md ├── README.md └── Source ├── .editorconfig ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── im │ └── delight │ └── android │ └── webview │ └── AdvancedWebView.java ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── im │ │ └── delight │ │ └── android │ │ └── examples │ │ └── webview │ │ └── MainActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_main.xml │ └── values │ ├── strings.xml │ └── styles.xml └── settings.gradle /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) delight.im (https://www.delight.im/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Migration.md: -------------------------------------------------------------------------------- 1 | # Migration 2 | 3 | ## From `v2.x.x` to `v3.x.x` 4 | 5 | * The license has been changed from the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) to the [MIT License](https://opensource.org/licenses/MIT). 6 | * The signature of `AdvancedWebView.Listener#onDownloadRequested` has changed from 7 | 8 | ```java 9 | void onDownloadRequested(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) 10 | ``` 11 | 12 | to 13 | 14 | ```java 15 | void onDownloadRequested(String url, String suggestedFilename, String mimeType, long contentLength, String contentDisposition, String userAgent) 16 | ``` 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdvancedWebView 2 | 3 | Enhanced WebView component for Android that works as intended out of the box 4 | 5 | ## Requirements 6 | 7 | * Android 2.2+ 8 | 9 | ## Installation 10 | 11 | * Add this library to your project 12 | * Declare the Gradle repository in your root `build.gradle` 13 | 14 | ```gradle 15 | allprojects { 16 | repositories { 17 | maven { url "https://jitpack.io" } 18 | } 19 | } 20 | ``` 21 | 22 | * Declare the Gradle dependency in your app module's `build.gradle` 23 | 24 | ```gradle 25 | dependencies { 26 | implementation 'com.github.delight-im:Android-AdvancedWebView:v3.2.1' 27 | } 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### AndroidManifest.xml 33 | 34 | ```xml 35 | 36 | ``` 37 | 38 | ### Layout (XML) 39 | 40 | ```xml 41 | 45 | ``` 46 | 47 | ### Activity (Java) 48 | 49 | #### Without Fragments 50 | 51 | ```java 52 | public class MyActivity extends Activity implements AdvancedWebView.Listener { 53 | 54 | private AdvancedWebView mWebView; 55 | 56 | @Override 57 | protected void onCreate(Bundle savedInstanceState) { 58 | super.onCreate(savedInstanceState); 59 | setContentView(R.layout.activity_my); 60 | 61 | mWebView = (AdvancedWebView) findViewById(R.id.webview); 62 | mWebView.setListener(this, this); 63 | mWebView.setMixedContentAllowed(false); 64 | mWebView.loadUrl("http://www.example.org/"); 65 | 66 | // ... 67 | } 68 | 69 | @SuppressLint("NewApi") 70 | @Override 71 | protected void onResume() { 72 | super.onResume(); 73 | mWebView.onResume(); 74 | // ... 75 | } 76 | 77 | @SuppressLint("NewApi") 78 | @Override 79 | protected void onPause() { 80 | mWebView.onPause(); 81 | // ... 82 | super.onPause(); 83 | } 84 | 85 | @Override 86 | protected void onDestroy() { 87 | mWebView.onDestroy(); 88 | // ... 89 | super.onDestroy(); 90 | } 91 | 92 | @Override 93 | protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 94 | super.onActivityResult(requestCode, resultCode, intent); 95 | mWebView.onActivityResult(requestCode, resultCode, intent); 96 | // ... 97 | } 98 | 99 | @Override 100 | public void onBackPressed() { 101 | if (!mWebView.onBackPressed()) { return; } 102 | // ... 103 | super.onBackPressed(); 104 | } 105 | 106 | @Override 107 | public void onPageStarted(String url, Bitmap favicon) { } 108 | 109 | @Override 110 | public void onPageFinished(String url) { } 111 | 112 | @Override 113 | public void onPageError(int errorCode, String description, String failingUrl) { } 114 | 115 | @Override 116 | public void onDownloadRequested(String url, String suggestedFilename, String mimeType, long contentLength, String contentDisposition, String userAgent) { } 117 | 118 | @Override 119 | public void onExternalPageRequest(String url) { } 120 | 121 | } 122 | ``` 123 | 124 | #### With Fragments (`android.app.Fragment`) 125 | 126 | **Note:** If you're using the `Fragment` class from the support library (`android.support.v4.app.Fragment`), please refer to the next section (see below) instead of this one. 127 | 128 | ```java 129 | public class MyFragment extends Fragment implements AdvancedWebView.Listener { 130 | 131 | private AdvancedWebView mWebView; 132 | 133 | public MyFragment() { } 134 | 135 | @Override 136 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 137 | View rootView = inflater.inflate(R.layout.fragment_my, container, false); 138 | 139 | mWebView = (AdvancedWebView) rootView.findViewById(R.id.webview); 140 | mWebView.setListener(this, this); 141 | mWebView.setMixedContentAllowed(false); 142 | mWebView.loadUrl("http://www.example.org/"); 143 | 144 | // ... 145 | 146 | return rootView; 147 | } 148 | 149 | @SuppressLint("NewApi") 150 | @Override 151 | public void onResume() { 152 | super.onResume(); 153 | mWebView.onResume(); 154 | // ... 155 | } 156 | 157 | @SuppressLint("NewApi") 158 | @Override 159 | public void onPause() { 160 | mWebView.onPause(); 161 | // ... 162 | super.onPause(); 163 | } 164 | 165 | @Override 166 | public void onDestroy() { 167 | mWebView.onDestroy(); 168 | // ... 169 | super.onDestroy(); 170 | } 171 | 172 | @Override 173 | public void onActivityResult(int requestCode, int resultCode, Intent intent) { 174 | super.onActivityResult(requestCode, resultCode, intent); 175 | mWebView.onActivityResult(requestCode, resultCode, intent); 176 | // ... 177 | } 178 | 179 | @Override 180 | public void onPageStarted(String url, Bitmap favicon) { } 181 | 182 | @Override 183 | public void onPageFinished(String url) { } 184 | 185 | @Override 186 | public void onPageError(int errorCode, String description, String failingUrl) { } 187 | 188 | @Override 189 | public void onDownloadRequested(String url, String suggestedFilename, String mimeType, long contentLength, String contentDisposition, String userAgent) { } 190 | 191 | @Override 192 | public void onExternalPageRequest(String url) { } 193 | 194 | } 195 | ``` 196 | 197 | #### With Fragments from the support library (`android.support.v4.app.Fragment`) 198 | 199 | * Use the code for normal `Fragment` usage as shown above 200 | * Change 201 | 202 | ```java 203 | mWebView.setListener(this, this); 204 | ``` 205 | 206 | to 207 | 208 | ```java 209 | mWebView.setListener(getActivity(), this); 210 | ``` 211 | 212 | * Add the following code to the parent `FragmentActivity` in order to forward the results from the `FragmentActivity` to the appropriate `Fragment` instance 213 | 214 | ```java 215 | public class MyActivity extends FragmentActivity implements AdvancedWebView.Listener { 216 | 217 | @Override 218 | public void onActivityResult(int requestCode, int resultCode, Intent intent) { 219 | super.onActivityResult(requestCode, resultCode, intent); 220 | if (mFragment != null) { 221 | mFragment.onActivityResult(requestCode, resultCode, intent); 222 | } 223 | } 224 | 225 | } 226 | ``` 227 | 228 | ### ProGuard (if enabled) 229 | 230 | ``` 231 | -keep class * extends android.webkit.WebChromeClient { *; } 232 | -dontwarn im.delight.android.webview.** 233 | ``` 234 | 235 | ### Cleartext (non-HTTPS) traffic 236 | 237 | If you want to serve sites or just single resources over plain `http` instead of `https`, there’s usually nothing to do if you’re targeting Android 8.1 (API level 27) or earlier. On Android 9 (API level 28) and later, however, [cleartext support is disabled by default](https://developer.android.com/training/articles/security-config). You may have to set `android:usesCleartextTraffic="true"` on the `` element in `AndroidManifest.xml` or provide a custom [network security configuration](https://developer.android.com/training/articles/security-config). 238 | 239 | ## Features 240 | 241 | * Optimized for best performance and security 242 | * Features are patched across Android versions 243 | * File uploads are handled automatically (check availability with `AdvancedWebView.isFileUploadAvailable()`) 244 | * Multiple file uploads via single input fields (`multiple` attribute in HTML) are supported on Android 5.0+. The application that is used to pick the files (i.e. usually a gallery or file manager app) must provide controls for selecting multiple files, which some apps don't. 245 | * JavaScript and WebStorage are enabled by default 246 | * Includes localizations for the 25 most widely spoken languages 247 | * Receive callbacks when pages start/finish loading or have errors 248 | 249 | ```java 250 | @Override 251 | public void onPageStarted(String url, Bitmap favicon) { 252 | // a new page started loading 253 | } 254 | 255 | @Override 256 | public void onPageFinished(String url) { 257 | // the new page finished loading 258 | } 259 | 260 | @Override 261 | public void onPageError(int errorCode, String description, String failingUrl) { 262 | // the new page failed to load 263 | } 264 | ``` 265 | 266 | * Downloads are handled automatically and can be listened to 267 | 268 | ```java 269 | @Override 270 | public void onDownloadRequested(String url, String suggestedFilename, String mimeType, long contentLength, String contentDisposition, String userAgent) { 271 | // some file is available for download 272 | // either handle the download yourself or use the code below 273 | 274 | if (AdvancedWebView.handleDownload(this, url, suggestedFilename)) { 275 | // download successfully handled 276 | } 277 | else { 278 | // download couldn't be handled because user has disabled download manager app on the device 279 | // TODO show some notice to the user 280 | } 281 | } 282 | ``` 283 | 284 | * Enable geolocation support (needs ``) 285 | 286 | ```java 287 | mWebView.setGeolocationEnabled(true); 288 | ``` 289 | 290 | * Add custom HTTP headers in addition to the ones sent by the web browser implementation 291 | 292 | ```java 293 | mWebView.addHttpHeader("X-Requested-With", "My wonderful app"); 294 | ``` 295 | 296 | * Define a custom set of permitted hostnames and receive callbacks for all other hostnames 297 | 298 | ```java 299 | mWebView.addPermittedHostname("example.org"); 300 | ``` 301 | 302 | and 303 | 304 | ```java 305 | @Override 306 | public void onExternalPageRequest(String url) { 307 | // the user tried to open a page from a non-permitted hostname 308 | } 309 | ``` 310 | 311 | * Prevent caching of HTML pages 312 | 313 | ```java 314 | boolean preventCaching = true; 315 | mWebView.loadUrl("http://www.example.org/", preventCaching); 316 | ``` 317 | 318 | * Check for alternative browsers installed on the device 319 | 320 | ```java 321 | if (AdvancedWebView.Browsers.hasAlternative(this)) { 322 | AdvancedWebView.Browsers.openUrl(this, "http://www.example.org/"); 323 | } 324 | ``` 325 | 326 | * Disable cookies 327 | 328 | ```java 329 | // disable third-party cookies only 330 | mWebView.setThirdPartyCookiesEnabled(false); 331 | // or disable cookies in general 332 | mWebView.setCookiesEnabled(false); 333 | ``` 334 | 335 | * Allow or disallow (both passive and active) mixed content (HTTP content being loaded inside HTTPS sites) 336 | 337 | ```java 338 | mWebView.setMixedContentAllowed(true); 339 | // or 340 | mWebView.setMixedContentAllowed(false); 341 | ``` 342 | 343 | * Switch between mobile and desktop mode 344 | 345 | ```java 346 | mWebView.setDesktopMode(true); 347 | // or 348 | // mWebView.setDesktopMode(false); 349 | ``` 350 | 351 | * Load HTML file from “assets” (e.g. at `app/src/main/assets/html/index.html`) 352 | 353 | ```java 354 | mWebView.loadUrl("file:///android_asset/html/index.html"); 355 | ``` 356 | 357 | * Load HTML file from SD card 358 | 359 | ```java 360 | // 361 | 362 | if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 363 | mWebView.getSettings().setAllowFileAccess(true); 364 | mWebView.loadUrl("file:///sdcard/Android/data/com.my.app/my_folder/index.html"); 365 | } 366 | ``` 367 | 368 | * Load HTML source text and display as page 369 | 370 | ```java 371 | myWebView.loadHtml("..."); 372 | 373 | // or 374 | 375 | final String myBaseUrl = "http://www.example.com/"; 376 | myWebView.loadHtml("...", myBaseUrl); 377 | ``` 378 | 379 | * Enable multi-window support 380 | 381 | ```java 382 | myWebView.getSettings().setSupportMultipleWindows(true); 383 | // myWebView.getSettings().setJavaScriptEnabled(true); 384 | // myWebView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); 385 | 386 | myWebView.setWebChromeClient(new WebChromeClient() { 387 | 388 | @Override 389 | public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { 390 | AdvancedWebView newWebView = new AdvancedWebView(MyNewActivity.this); 391 | // myParentLayout.addView(newWebView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 392 | WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; 393 | transport.setWebView(newWebView); 394 | resultMsg.sendToTarget(); 395 | 396 | return true; 397 | } 398 | 399 | } 400 | ``` 401 | 402 | ## Contributing 403 | 404 | All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed. 405 | 406 | ## License 407 | 408 | This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT). 409 | -------------------------------------------------------------------------------- /Source/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /Source/.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/ 33 | 34 | # Custom 35 | .idea/ 36 | *.iml 37 | -------------------------------------------------------------------------------- /Source/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.0.1' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | task clean(type: Delete) { 25 | delete rootProject.buildDir 26 | } 27 | -------------------------------------------------------------------------------- /Source/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | android.enableJetifier=true 20 | android.useAndroidX=true 21 | -------------------------------------------------------------------------------- /Source/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacoTheDank/Android-AdvancedWebView/e461b74d34d6ef099b27a35d38ce5c9058e37d94/Source/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Source/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /Source/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /Source/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /Source/library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion '29.0.3' 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 28 10 | } 11 | 12 | compileOptions { 13 | encoding "UTF-8" 14 | sourceCompatibility JavaVersion.VERSION_1_8 15 | targetCompatibility JavaVersion.VERSION_1_8 16 | } 17 | 18 | packagingOptions { 19 | exclude 'META-INF/*' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/webview/AdvancedWebView.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.webview; 2 | 3 | /* 4 | * Android-AdvancedWebView (https://github.com/delight-im/Android-AdvancedWebView) 5 | * Copyright (c) delight.im (https://www.delight.im/) 6 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 7 | */ 8 | 9 | import android.app.Activity; 10 | import android.app.DownloadManager; 11 | import android.app.DownloadManager.Request; 12 | import android.app.Fragment; 13 | import android.content.ActivityNotFoundException; 14 | import android.content.Context; 15 | import android.content.Intent; 16 | import android.content.pm.ApplicationInfo; 17 | import android.content.pm.PackageManager; 18 | import android.graphics.Bitmap; 19 | import android.net.Uri; 20 | import android.net.http.SslError; 21 | import android.os.Build; 22 | import android.os.Environment; 23 | import android.os.Message; 24 | import android.util.AttributeSet; 25 | import android.util.Base64; 26 | import android.view.KeyEvent; 27 | import android.view.View; 28 | import android.view.ViewGroup; 29 | import android.webkit.ClientCertRequest; 30 | import android.webkit.ConsoleMessage; 31 | import android.webkit.CookieManager; 32 | import android.webkit.GeolocationPermissions.Callback; 33 | import android.webkit.HttpAuthHandler; 34 | import android.webkit.JsPromptResult; 35 | import android.webkit.JsResult; 36 | import android.webkit.PermissionRequest; 37 | import android.webkit.SslErrorHandler; 38 | import android.webkit.URLUtil; 39 | import android.webkit.ValueCallback; 40 | import android.webkit.WebChromeClient; 41 | import android.webkit.WebResourceRequest; 42 | import android.webkit.WebResourceResponse; 43 | import android.webkit.WebSettings; 44 | import android.webkit.WebStorage.QuotaUpdater; 45 | import android.webkit.WebView; 46 | import android.webkit.WebViewClient; 47 | import android.widget.Toast; 48 | 49 | import java.io.UnsupportedEncodingException; 50 | import java.lang.ref.WeakReference; 51 | import java.util.Arrays; 52 | import java.util.Collection; 53 | import java.util.HashMap; 54 | import java.util.LinkedList; 55 | import java.util.List; 56 | import java.util.Locale; 57 | import java.util.Map; 58 | import java.util.MissingResourceException; 59 | 60 | /** 61 | * Advanced WebView component for Android that works as intended out of the box 62 | */ 63 | public class AdvancedWebView extends WebView { 64 | 65 | public static final String PACKAGE_NAME_DOWNLOAD_MANAGER = "com.android.providers.downloads"; 66 | protected static final int REQUEST_CODE_FILE_PICKER = 51426; 67 | protected static final String DATABASES_SUB_FOLDER = "/databases"; 68 | protected static final String LANGUAGE_DEFAULT_ISO3 = "eng"; 69 | protected static final String CHARSET_DEFAULT = "UTF-8"; 70 | /** 71 | * Alternative browsers that have their own rendering engine and *may* be installed on this device 72 | */ 73 | protected static final String[] ALTERNATIVE_BROWSERS = new String[]{"org.mozilla.firefox", "com.android.chrome", "com.opera.browser", "org.mozilla.firefox_beta", "com.chrome.beta", "com.opera.browser.beta"}; 74 | protected final List mPermittedHostnames = new LinkedList<>(); 75 | protected final Map mHttpHeaders = new HashMap<>(); 76 | protected WeakReference mActivity; 77 | protected WeakReference mFragment; 78 | protected Listener mListener; 79 | /** 80 | * File upload callback for platform versions prior to Android 5.0 81 | */ 82 | protected ValueCallback mFileUploadCallbackFirst; 83 | /** 84 | * File upload callback for Android 5.0+ 85 | */ 86 | protected ValueCallback mFileUploadCallbackSecond; 87 | protected long mLastError; 88 | protected String mLanguageIso3; 89 | protected int mRequestCodeFilePicker = REQUEST_CODE_FILE_PICKER; 90 | protected WebViewClient mCustomWebViewClient; 91 | protected WebChromeClient mCustomWebChromeClient; 92 | protected boolean mGeolocationEnabled; 93 | protected String mUploadableFileTypes = "*/*"; 94 | 95 | public AdvancedWebView(Context context) { 96 | super(context); 97 | init(context); 98 | } 99 | 100 | public AdvancedWebView(Context context, AttributeSet attrs) { 101 | super(context, attrs); 102 | init(context); 103 | } 104 | 105 | public AdvancedWebView(Context context, AttributeSet attrs, int defStyleAttr) { 106 | super(context, attrs, defStyleAttr); 107 | init(context); 108 | } 109 | 110 | protected static void setAllowAccessFromFileUrls(final WebSettings webSettings) { 111 | if (Build.VERSION.SDK_INT >= 16) { 112 | webSettings.setAllowFileAccessFromFileURLs(false); 113 | webSettings.setAllowUniversalAccessFromFileURLs(false); 114 | } 115 | } 116 | 117 | protected static String makeUrlUnique(final String url) { 118 | StringBuilder unique = new StringBuilder(); 119 | unique.append(url); 120 | 121 | if (url.contains("?")) { 122 | unique.append('&'); 123 | } else { 124 | if (url.lastIndexOf('/') <= 7) { 125 | unique.append('/'); 126 | } 127 | unique.append('?'); 128 | } 129 | 130 | unique.append(System.currentTimeMillis()); 131 | unique.append('='); 132 | unique.append(1); 133 | 134 | return unique.toString(); 135 | } 136 | 137 | protected static String getLanguageIso3() { 138 | try { 139 | return Locale.getDefault().getISO3Language().toLowerCase(Locale.US); 140 | } catch (MissingResourceException e) { 141 | return LANGUAGE_DEFAULT_ISO3; 142 | } 143 | } 144 | 145 | protected static String decodeBase64(final String base64) throws IllegalArgumentException, UnsupportedEncodingException { 146 | final byte[] bytes = Base64.decode(base64, Base64.DEFAULT); 147 | return new String(bytes, CHARSET_DEFAULT); 148 | } 149 | 150 | /** 151 | * Returns whether file uploads can be used on the current device (generally all platform versions except for 4.4) 152 | * 153 | * @return whether file uploads can be used 154 | */ 155 | public static boolean isFileUploadAvailable() { 156 | return isFileUploadAvailable(false); 157 | } 158 | 159 | /** 160 | * Returns whether file uploads can be used on the current device (generally all platform versions except for 4.4) 161 | *

162 | * On Android 4.4.3/4.4.4, file uploads may be possible but will come with a wrong MIME type 163 | * 164 | * @param needsCorrectMimeType whether a correct MIME type is required for file uploads or `application/octet-stream` is acceptable 165 | * @return whether file uploads can be used 166 | */ 167 | public static boolean isFileUploadAvailable(final boolean needsCorrectMimeType) { 168 | if (Build.VERSION.SDK_INT == 19) { 169 | final String platformVersion = (Build.VERSION.RELEASE == null) ? "" : Build.VERSION.RELEASE; 170 | 171 | return !needsCorrectMimeType && (platformVersion.startsWith("4.4.3") || platformVersion.startsWith("4.4.4")); 172 | } else { 173 | return true; 174 | } 175 | } 176 | 177 | /** 178 | * Handles a download by loading the file from `fromUrl` and saving it to `toFilename` on the external storage 179 | *

180 | * This requires the two permissions `android.permission.INTERNET` and `android.permission.WRITE_EXTERNAL_STORAGE` 181 | *

182 | * Only supported on API level 9 (Android 2.3) and above 183 | * 184 | * @param context a valid `Context` reference 185 | * @param fromUrl the URL of the file to download, e.g. the one from `AdvancedWebView.onDownloadRequested(...)` 186 | * @param toFilename the name of the destination file where the download should be saved, e.g. `myImage.jpg` 187 | * @return whether the download has been successfully handled or not 188 | * @throws IllegalStateException if the storage or the target directory could not be found or accessed 189 | */ 190 | public static boolean handleDownload(final Context context, final String fromUrl, final String toFilename) { 191 | 192 | final Request request = new Request(Uri.parse(fromUrl)); 193 | request.allowScanningByMediaScanner(); 194 | request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); 195 | request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, toFilename); 196 | 197 | final DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); 198 | try { 199 | try { 200 | dm.enqueue(request); 201 | } catch (SecurityException e) { 202 | request.setNotificationVisibility(Request.VISIBILITY_VISIBLE); 203 | dm.enqueue(request); 204 | } 205 | 206 | return true; 207 | } catch (SecurityException e) { 208 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 209 | Toast.makeText(context, "Please enable STORAGE permission!", 210 | Toast.LENGTH_LONG).show(); 211 | openAppSettings(context); 212 | } 213 | return false; 214 | } 215 | // if the download manager app has been disabled on the device 216 | catch (IllegalArgumentException e) { 217 | // show the settings screen where the user can enable the download manager app again 218 | openAppSettings(context); 219 | 220 | return false; 221 | } 222 | } 223 | 224 | private static void openAppSettings(final Context context) { 225 | try { 226 | final Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 227 | intent.setData(Uri.parse("package:" + AdvancedWebView.PACKAGE_NAME_DOWNLOAD_MANAGER)); 228 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 229 | 230 | context.startActivity(intent); 231 | 232 | } catch (Exception ignored) { 233 | } 234 | } 235 | 236 | public void setListener(final Activity activity, final Listener listener) { 237 | setListener(activity, listener, REQUEST_CODE_FILE_PICKER); 238 | } 239 | 240 | public void setListener(final Activity activity, final Listener listener, final int requestCodeFilePicker) { 241 | if (activity != null) { 242 | mActivity = new WeakReference<>(activity); 243 | } else { 244 | mActivity = null; 245 | } 246 | 247 | setListener(listener, requestCodeFilePicker); 248 | } 249 | 250 | protected void setListener(final Listener listener, final int requestCodeFilePicker) { 251 | mListener = listener; 252 | mRequestCodeFilePicker = requestCodeFilePicker; 253 | } 254 | 255 | @Override 256 | public void setWebViewClient(final WebViewClient client) { 257 | mCustomWebViewClient = client; 258 | } 259 | 260 | @Override 261 | public void setWebChromeClient(final WebChromeClient client) { 262 | mCustomWebChromeClient = client; 263 | } 264 | 265 | public void setGeolocationEnabled(final boolean enabled) { 266 | if (enabled) { 267 | getSettings().setJavaScriptEnabled(true); 268 | getSettings().setGeolocationEnabled(true); 269 | setGeolocationDatabasePath(); 270 | } 271 | 272 | mGeolocationEnabled = enabled; 273 | } 274 | 275 | protected void setGeolocationDatabasePath() { 276 | final Activity activity; 277 | 278 | if (mFragment != null && mFragment.get() != null && mFragment.get().getActivity() != null) { 279 | activity = mFragment.get().getActivity(); 280 | } else if (mActivity != null && mActivity.get() != null) { 281 | activity = mActivity.get(); 282 | } else { 283 | return; 284 | } 285 | 286 | if (activity != null) { 287 | getSettings().setGeolocationDatabasePath(activity.getFilesDir().getPath()); 288 | } 289 | } 290 | 291 | public void setUploadableFileTypes(final String mimeType) { 292 | mUploadableFileTypes = mimeType; 293 | } 294 | 295 | /** 296 | * Loads and displays the provided HTML source text 297 | * 298 | * @param html the HTML source text to load 299 | */ 300 | public void loadHtml(final String html) { 301 | loadHtml(html, null); 302 | } 303 | 304 | /** 305 | * Loads and displays the provided HTML source text 306 | * 307 | * @param html the HTML source text to load 308 | * @param baseUrl the URL to use as the page's base URL 309 | */ 310 | public void loadHtml(final String html, final String baseUrl) { 311 | loadHtml(html, baseUrl, null); 312 | } 313 | 314 | /** 315 | * Loads and displays the provided HTML source text 316 | * 317 | * @param html the HTML source text to load 318 | * @param baseUrl the URL to use as the page's base URL 319 | * @param historyUrl the URL to use for the page's history entry 320 | */ 321 | public void loadHtml(final String html, final String baseUrl, final String historyUrl) { 322 | loadHtml(html, baseUrl, historyUrl, "utf-8"); 323 | } 324 | 325 | /** 326 | * Loads and displays the provided HTML source text 327 | * 328 | * @param html the HTML source text to load 329 | * @param baseUrl the URL to use as the page's base URL 330 | * @param historyUrl the URL to use for the page's history entry 331 | * @param encoding the encoding or charset of the HTML source text 332 | */ 333 | public void loadHtml(final String html, final String baseUrl, final String historyUrl, final String encoding) { 334 | loadDataWithBaseURL(baseUrl, html, "text/html", encoding, historyUrl); 335 | } 336 | 337 | public void onResume() { 338 | super.onResume(); 339 | resumeTimers(); 340 | } 341 | 342 | public void onPause() { 343 | pauseTimers(); 344 | super.onPause(); 345 | } 346 | 347 | public void onDestroy() { 348 | // try to remove this view from its parent first 349 | try { 350 | ((ViewGroup) getParent()).removeView(this); 351 | } catch (Exception ignored) { 352 | } 353 | 354 | // then try to remove all child views from this view 355 | try { 356 | removeAllViews(); 357 | } catch (Exception ignored) { 358 | } 359 | 360 | // and finally destroy this view 361 | destroy(); 362 | } 363 | 364 | public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { 365 | if (requestCode == mRequestCodeFilePicker) { 366 | if (resultCode == Activity.RESULT_OK) { 367 | if (intent != null) { 368 | if (mFileUploadCallbackFirst != null) { 369 | mFileUploadCallbackFirst.onReceiveValue(intent.getData()); 370 | mFileUploadCallbackFirst = null; 371 | } else if (mFileUploadCallbackSecond != null) { 372 | Uri[] dataUris = null; 373 | 374 | try { 375 | if (intent.getDataString() != null) { 376 | dataUris = new Uri[]{Uri.parse(intent.getDataString())}; 377 | } else { 378 | if (Build.VERSION.SDK_INT >= 16) { 379 | if (intent.getClipData() != null) { 380 | final int numSelectedFiles = intent.getClipData().getItemCount(); 381 | 382 | dataUris = new Uri[numSelectedFiles]; 383 | 384 | for (int i = 0; i < numSelectedFiles; i++) { 385 | dataUris[i] = intent.getClipData().getItemAt(i).getUri(); 386 | } 387 | } 388 | } 389 | } 390 | } catch (Exception ignored) { 391 | } 392 | 393 | mFileUploadCallbackSecond.onReceiveValue(dataUris); 394 | mFileUploadCallbackSecond = null; 395 | } 396 | } 397 | } else { 398 | if (mFileUploadCallbackFirst != null) { 399 | mFileUploadCallbackFirst.onReceiveValue(null); 400 | mFileUploadCallbackFirst = null; 401 | } else if (mFileUploadCallbackSecond != null) { 402 | mFileUploadCallbackSecond.onReceiveValue(null); 403 | mFileUploadCallbackSecond = null; 404 | } 405 | } 406 | } 407 | } 408 | 409 | /** 410 | * Adds an additional HTTP header that will be sent along with every HTTP `GET` request 411 | *

412 | * This does only affect the main requests, not the requests to included resources (e.g. images) 413 | *

414 | * If you later want to delete an HTTP header that was previously added this way, call `removeHttpHeader()` 415 | *

416 | * The `WebView` implementation may in some cases overwrite headers that you set or unset 417 | * 418 | * @param name the name of the HTTP header to add 419 | * @param value the value of the HTTP header to send 420 | */ 421 | public void addHttpHeader(final String name, final String value) { 422 | mHttpHeaders.put(name, value); 423 | } 424 | 425 | /** 426 | * Removes one of the HTTP headers that have previously been added via `addHttpHeader()` 427 | *

428 | * If you want to unset a pre-defined header, set it to an empty string with `addHttpHeader()` instead 429 | *

430 | * The `WebView` implementation may in some cases overwrite headers that you set or unset 431 | * 432 | * @param name the name of the HTTP header to remove 433 | */ 434 | public void removeHttpHeader(final String name) { 435 | mHttpHeaders.remove(name); 436 | } 437 | 438 | public void addPermittedHostname(String hostname) { 439 | mPermittedHostnames.add(hostname); 440 | } 441 | 442 | public void addPermittedHostnames(Collection collection) { 443 | mPermittedHostnames.addAll(collection); 444 | } 445 | 446 | public List getPermittedHostnames() { 447 | return mPermittedHostnames; 448 | } 449 | 450 | public void removePermittedHostname(String hostname) { 451 | mPermittedHostnames.remove(hostname); 452 | } 453 | 454 | public void clearPermittedHostnames() { 455 | mPermittedHostnames.clear(); 456 | } 457 | 458 | public boolean onBackPressed() { 459 | if (canGoBack()) { 460 | goBack(); 461 | return false; 462 | } else { 463 | return true; 464 | } 465 | } 466 | 467 | public void setCookiesEnabled(final boolean enabled) { 468 | CookieManager.getInstance().setAcceptCookie(enabled); 469 | } 470 | 471 | public void setThirdPartyCookiesEnabled(final boolean enabled) { 472 | if (Build.VERSION.SDK_INT >= 21) { 473 | CookieManager.getInstance().setAcceptThirdPartyCookies(this, enabled); 474 | } 475 | } 476 | 477 | public void setMixedContentAllowed(final boolean allowed) { 478 | setMixedContentAllowed(getSettings(), allowed); 479 | } 480 | 481 | protected void setMixedContentAllowed(final WebSettings webSettings, final boolean allowed) { 482 | if (Build.VERSION.SDK_INT >= 21) { 483 | webSettings.setMixedContentMode(allowed ? WebSettings.MIXED_CONTENT_ALWAYS_ALLOW : WebSettings.MIXED_CONTENT_NEVER_ALLOW); 484 | } 485 | } 486 | 487 | public void setDesktopMode(final boolean enabled) { 488 | final WebSettings webSettings = getSettings(); 489 | 490 | final String newUserAgent; 491 | if (enabled) { 492 | newUserAgent = webSettings.getUserAgentString().replace("Mobile", "eliboM").replace("Android", "diordnA"); 493 | } else { 494 | newUserAgent = webSettings.getUserAgentString().replace("eliboM", "Mobile").replace("diordnA", "Android"); 495 | } 496 | 497 | webSettings.setUserAgentString(newUserAgent); 498 | webSettings.setUseWideViewPort(enabled); 499 | webSettings.setLoadWithOverviewMode(enabled); 500 | webSettings.setSupportZoom(enabled); 501 | webSettings.setBuiltInZoomControls(enabled); 502 | } 503 | 504 | protected void init(Context context) { 505 | // in IDE's preview mode 506 | if (isInEditMode()) { 507 | // do not run the code from this method 508 | return; 509 | } 510 | 511 | if (context instanceof Activity) { 512 | mActivity = new WeakReference<>((Activity) context); 513 | } 514 | 515 | mLanguageIso3 = getLanguageIso3(); 516 | 517 | setFocusable(true); 518 | setFocusableInTouchMode(true); 519 | 520 | setSaveEnabled(true); 521 | 522 | 523 | final WebSettings webSettings = getSettings(); 524 | webSettings.setAllowFileAccess(false); 525 | setAllowAccessFromFileUrls(webSettings); 526 | webSettings.setBuiltInZoomControls(false); 527 | webSettings.setJavaScriptEnabled(true); 528 | webSettings.setDomStorageEnabled(true); 529 | 530 | if (Build.VERSION.SDK_INT < 18) { 531 | webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH); 532 | } 533 | webSettings.setDatabaseEnabled(true); 534 | 535 | if (Build.VERSION.SDK_INT < 19) { 536 | final String filesDir = context.getFilesDir().getPath(); 537 | final String databaseDir = filesDir.substring(0, filesDir.lastIndexOf("/")) + DATABASES_SUB_FOLDER; 538 | webSettings.setDatabasePath(databaseDir); 539 | } 540 | 541 | if (Build.VERSION.SDK_INT >= 26) { 542 | // this should make render a bit faster 543 | this.setRendererPriorityPolicy(RENDERER_PRIORITY_IMPORTANT, false); 544 | } 545 | 546 | if (Build.VERSION.SDK_INT >= 21) { 547 | webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); 548 | } 549 | 550 | setThirdPartyCookiesEnabled(true); 551 | 552 | super.setWebViewClient(new WebViewClient() { 553 | 554 | @Override 555 | public void onPageStarted(WebView view, String url, Bitmap favicon) { 556 | if (!hasError()) { 557 | if (mListener != null) { 558 | mListener.onPageStarted(url, favicon); 559 | } 560 | } 561 | 562 | if (mCustomWebViewClient != null) { 563 | mCustomWebViewClient.onPageStarted(view, url, favicon); 564 | } 565 | } 566 | 567 | @Override 568 | public void onPageFinished(WebView view, String url) { 569 | if (!hasError()) { 570 | if (mListener != null) { 571 | mListener.onPageFinished(url); 572 | } 573 | } 574 | 575 | if (mCustomWebViewClient != null) { 576 | mCustomWebViewClient.onPageFinished(view, url); 577 | } 578 | } 579 | 580 | @Override 581 | public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { 582 | setLastError(); 583 | 584 | if (mListener != null) { 585 | mListener.onPageError(errorCode, description, failingUrl); 586 | } 587 | 588 | if (mCustomWebViewClient != null) { 589 | mCustomWebViewClient.onReceivedError(view, errorCode, description, failingUrl); 590 | } 591 | } 592 | 593 | @Override 594 | public boolean shouldOverrideUrlLoading(final WebView view, final String url) { 595 | if (!isPermittedUrl(url)) { 596 | // if a listener is available 597 | if (mListener != null) { 598 | // inform the listener about the request 599 | mListener.onExternalPageRequest(url); 600 | } 601 | 602 | // cancel the original request 603 | return true; 604 | } 605 | 606 | // if there is a user-specified handler available 607 | if (mCustomWebViewClient != null) { 608 | // if the user-specified handler asks to override the request 609 | if (mCustomWebViewClient.shouldOverrideUrlLoading(view, url)) { 610 | // cancel the original request 611 | return true; 612 | } 613 | } 614 | 615 | final Uri uri = Uri.parse(url); 616 | final String scheme = uri.getScheme(); 617 | 618 | if (scheme != null) { 619 | final Intent externalSchemeIntent; 620 | 621 | switch (scheme) { 622 | case "tel": 623 | externalSchemeIntent = new Intent(Intent.ACTION_DIAL, uri); 624 | break; 625 | case "sms": 626 | case "mailto": 627 | externalSchemeIntent = new Intent(Intent.ACTION_SENDTO, uri); 628 | break; 629 | case "whatsapp": 630 | externalSchemeIntent = new Intent(Intent.ACTION_SENDTO, uri); 631 | externalSchemeIntent.setPackage("com.whatsapp"); 632 | break; 633 | default: 634 | externalSchemeIntent = null; 635 | break; 636 | } 637 | 638 | if (externalSchemeIntent != null) { 639 | externalSchemeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 640 | 641 | try { 642 | if (mActivity != null && mActivity.get() != null) { 643 | mActivity.get().startActivity(externalSchemeIntent); 644 | } else { 645 | getContext().startActivity(externalSchemeIntent); 646 | } 647 | } catch (ActivityNotFoundException ignored) { 648 | } 649 | 650 | // cancel the original request 651 | return true; 652 | } 653 | } 654 | 655 | // route the request through the custom URL loading method 656 | view.loadUrl(url); 657 | 658 | // cancel the original request 659 | return true; 660 | } 661 | 662 | @Override 663 | public void onLoadResource(WebView view, String url) { 664 | if (mCustomWebViewClient != null) { 665 | mCustomWebViewClient.onLoadResource(view, url); 666 | } else { 667 | super.onLoadResource(view, url); 668 | } 669 | } 670 | 671 | public WebResourceResponse shouldInterceptRequest(WebView view, String url) { 672 | if (mCustomWebViewClient != null) { 673 | return mCustomWebViewClient.shouldInterceptRequest(view, url); 674 | } else { 675 | return super.shouldInterceptRequest(view, url); 676 | } 677 | } 678 | 679 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 680 | if (Build.VERSION.SDK_INT >= 21) { 681 | if (mCustomWebViewClient != null) { 682 | return mCustomWebViewClient.shouldInterceptRequest(view, request); 683 | } else { 684 | return super.shouldInterceptRequest(view, request); 685 | } 686 | } else { 687 | return null; 688 | } 689 | } 690 | 691 | @Override 692 | public void onFormResubmission(WebView view, Message dontResend, Message resend) { 693 | if (mCustomWebViewClient != null) { 694 | mCustomWebViewClient.onFormResubmission(view, dontResend, resend); 695 | } else { 696 | super.onFormResubmission(view, dontResend, resend); 697 | } 698 | } 699 | 700 | @Override 701 | public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { 702 | if (mCustomWebViewClient != null) { 703 | mCustomWebViewClient.doUpdateVisitedHistory(view, url, isReload); 704 | } else { 705 | super.doUpdateVisitedHistory(view, url, isReload); 706 | } 707 | } 708 | 709 | @Override 710 | public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { 711 | if (mCustomWebViewClient != null) { 712 | mCustomWebViewClient.onReceivedSslError(view, handler, error); 713 | } else { 714 | super.onReceivedSslError(view, handler, error); 715 | } 716 | } 717 | 718 | public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) { 719 | if (Build.VERSION.SDK_INT >= 21) { 720 | if (mCustomWebViewClient != null) { 721 | mCustomWebViewClient.onReceivedClientCertRequest(view, request); 722 | } else { 723 | super.onReceivedClientCertRequest(view, request); 724 | } 725 | } 726 | } 727 | 728 | @Override 729 | public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { 730 | if (mCustomWebViewClient != null) { 731 | mCustomWebViewClient.onReceivedHttpAuthRequest(view, handler, host, realm); 732 | } else { 733 | super.onReceivedHttpAuthRequest(view, handler, host, realm); 734 | } 735 | } 736 | 737 | @Override 738 | public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { 739 | if (mCustomWebViewClient != null) { 740 | return mCustomWebViewClient.shouldOverrideKeyEvent(view, event); 741 | } else { 742 | return super.shouldOverrideKeyEvent(view, event); 743 | } 744 | } 745 | 746 | @Override 747 | public void onUnhandledKeyEvent(WebView view, KeyEvent event) { 748 | if (mCustomWebViewClient != null) { 749 | mCustomWebViewClient.onUnhandledKeyEvent(view, event); 750 | } else { 751 | super.onUnhandledKeyEvent(view, event); 752 | } 753 | } 754 | 755 | @Override 756 | public void onScaleChanged(WebView view, float oldScale, float newScale) { 757 | if (mCustomWebViewClient != null) { 758 | mCustomWebViewClient.onScaleChanged(view, oldScale, newScale); 759 | } else { 760 | super.onScaleChanged(view, oldScale, newScale); 761 | } 762 | } 763 | 764 | public void onReceivedLoginRequest(WebView view, String realm, String account, String args) { 765 | if (mCustomWebViewClient != null) { 766 | mCustomWebViewClient.onReceivedLoginRequest(view, realm, account, args); 767 | } else { 768 | super.onReceivedLoginRequest(view, realm, account, args); 769 | } 770 | } 771 | 772 | }); 773 | 774 | super.setWebChromeClient(new WebChromeClient() { 775 | 776 | // file upload callback (Android 5.0 (API level 21) -- current) (public method) 777 | public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) { 778 | if (Build.VERSION.SDK_INT >= 21) { 779 | final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE; 780 | 781 | openFileInput(filePathCallback, allowMultiple); 782 | 783 | return true; 784 | } else { 785 | return false; 786 | } 787 | } 788 | 789 | @Override 790 | public void onProgressChanged(WebView view, int newProgress) { 791 | if (mCustomWebChromeClient != null) { 792 | mCustomWebChromeClient.onProgressChanged(view, newProgress); 793 | } else { 794 | super.onProgressChanged(view, newProgress); 795 | } 796 | } 797 | 798 | @Override 799 | public void onReceivedTitle(WebView view, String title) { 800 | if (mCustomWebChromeClient != null) { 801 | mCustomWebChromeClient.onReceivedTitle(view, title); 802 | } else { 803 | super.onReceivedTitle(view, title); 804 | } 805 | } 806 | 807 | @Override 808 | public void onReceivedIcon(WebView view, Bitmap icon) { 809 | if (mCustomWebChromeClient != null) { 810 | mCustomWebChromeClient.onReceivedIcon(view, icon); 811 | } else { 812 | super.onReceivedIcon(view, icon); 813 | } 814 | } 815 | 816 | @Override 817 | public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) { 818 | if (mCustomWebChromeClient != null) { 819 | mCustomWebChromeClient.onReceivedTouchIconUrl(view, url, precomposed); 820 | } else { 821 | super.onReceivedTouchIconUrl(view, url, precomposed); 822 | } 823 | } 824 | 825 | @Override 826 | public void onShowCustomView(View view, CustomViewCallback callback) { 827 | if (mCustomWebChromeClient != null) { 828 | mCustomWebChromeClient.onShowCustomView(view, callback); 829 | } else { 830 | super.onShowCustomView(view, callback); 831 | } 832 | } 833 | 834 | public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) { 835 | if (Build.VERSION.SDK_INT >= 14) { 836 | if (mCustomWebChromeClient != null) { 837 | mCustomWebChromeClient.onShowCustomView(view, requestedOrientation, callback); 838 | } else { 839 | super.onShowCustomView(view, requestedOrientation, callback); 840 | } 841 | } 842 | } 843 | 844 | @Override 845 | public void onHideCustomView() { 846 | if (mCustomWebChromeClient != null) { 847 | mCustomWebChromeClient.onHideCustomView(); 848 | } else { 849 | super.onHideCustomView(); 850 | } 851 | } 852 | 853 | @Override 854 | public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { 855 | if (mCustomWebChromeClient != null) { 856 | return mCustomWebChromeClient.onCreateWindow(view, isDialog, isUserGesture, resultMsg); 857 | } else { 858 | return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg); 859 | } 860 | } 861 | 862 | @Override 863 | public void onRequestFocus(WebView view) { 864 | if (mCustomWebChromeClient != null) { 865 | mCustomWebChromeClient.onRequestFocus(view); 866 | } else { 867 | super.onRequestFocus(view); 868 | } 869 | } 870 | 871 | @Override 872 | public void onCloseWindow(WebView window) { 873 | if (mCustomWebChromeClient != null) { 874 | mCustomWebChromeClient.onCloseWindow(window); 875 | } else { 876 | super.onCloseWindow(window); 877 | } 878 | } 879 | 880 | @Override 881 | public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 882 | if (mCustomWebChromeClient != null) { 883 | return mCustomWebChromeClient.onJsAlert(view, url, message, result); 884 | } else { 885 | return super.onJsAlert(view, url, message, result); 886 | } 887 | } 888 | 889 | @Override 890 | public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 891 | if (mCustomWebChromeClient != null) { 892 | return mCustomWebChromeClient.onJsConfirm(view, url, message, result); 893 | } else { 894 | return super.onJsConfirm(view, url, message, result); 895 | } 896 | } 897 | 898 | @Override 899 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 900 | if (mCustomWebChromeClient != null) { 901 | return mCustomWebChromeClient.onJsPrompt(view, url, message, defaultValue, result); 902 | } else { 903 | return super.onJsPrompt(view, url, message, defaultValue, result); 904 | } 905 | } 906 | 907 | @Override 908 | public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) { 909 | if (mCustomWebChromeClient != null) { 910 | return mCustomWebChromeClient.onJsBeforeUnload(view, url, message, result); 911 | } else { 912 | return super.onJsBeforeUnload(view, url, message, result); 913 | } 914 | } 915 | 916 | @Override 917 | public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) { 918 | if (mGeolocationEnabled) { 919 | callback.invoke(origin, true, false); 920 | } else { 921 | if (mCustomWebChromeClient != null) { 922 | mCustomWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); 923 | } else { 924 | super.onGeolocationPermissionsShowPrompt(origin, callback); 925 | } 926 | } 927 | } 928 | 929 | @Override 930 | public void onGeolocationPermissionsHidePrompt() { 931 | if (mCustomWebChromeClient != null) { 932 | mCustomWebChromeClient.onGeolocationPermissionsHidePrompt(); 933 | } else { 934 | super.onGeolocationPermissionsHidePrompt(); 935 | } 936 | } 937 | 938 | public void onPermissionRequest(PermissionRequest request) { 939 | if (Build.VERSION.SDK_INT >= 21) { 940 | if (mCustomWebChromeClient != null) { 941 | mCustomWebChromeClient.onPermissionRequest(request); 942 | } else { 943 | super.onPermissionRequest(request); 944 | } 945 | } 946 | } 947 | 948 | public void onPermissionRequestCanceled(PermissionRequest request) { 949 | if (Build.VERSION.SDK_INT >= 21) { 950 | if (mCustomWebChromeClient != null) { 951 | mCustomWebChromeClient.onPermissionRequestCanceled(request); 952 | } else { 953 | super.onPermissionRequestCanceled(request); 954 | } 955 | } 956 | } 957 | 958 | @Override 959 | public boolean onJsTimeout() { 960 | if (mCustomWebChromeClient != null) { 961 | return mCustomWebChromeClient.onJsTimeout(); 962 | } else { 963 | return super.onJsTimeout(); 964 | } 965 | } 966 | 967 | @Override 968 | public void onConsoleMessage(String message, int lineNumber, String sourceID) { 969 | if (mCustomWebChromeClient != null) { 970 | mCustomWebChromeClient.onConsoleMessage(message, lineNumber, sourceID); 971 | } else { 972 | super.onConsoleMessage(message, lineNumber, sourceID); 973 | } 974 | } 975 | 976 | @Override 977 | public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 978 | if (mCustomWebChromeClient != null) { 979 | return mCustomWebChromeClient.onConsoleMessage(consoleMessage); 980 | } else { 981 | return super.onConsoleMessage(consoleMessage); 982 | } 983 | } 984 | 985 | @Override 986 | public Bitmap getDefaultVideoPoster() { 987 | if (mCustomWebChromeClient != null) { 988 | return mCustomWebChromeClient.getDefaultVideoPoster(); 989 | } else { 990 | return super.getDefaultVideoPoster(); 991 | } 992 | } 993 | 994 | @Override 995 | public View getVideoLoadingProgressView() { 996 | if (mCustomWebChromeClient != null) { 997 | return mCustomWebChromeClient.getVideoLoadingProgressView(); 998 | } else { 999 | return super.getVideoLoadingProgressView(); 1000 | } 1001 | } 1002 | 1003 | @Override 1004 | public void getVisitedHistory(ValueCallback callback) { 1005 | if (mCustomWebChromeClient != null) { 1006 | mCustomWebChromeClient.getVisitedHistory(callback); 1007 | } else { 1008 | super.getVisitedHistory(callback); 1009 | } 1010 | } 1011 | 1012 | @Override 1013 | public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, long estimatedDatabaseSize, long totalQuota, QuotaUpdater quotaUpdater) { 1014 | if (mCustomWebChromeClient != null) { 1015 | mCustomWebChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater); 1016 | } else { 1017 | super.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater); 1018 | } 1019 | } 1020 | 1021 | @Override 1022 | public void onReachedMaxAppCacheSize(long requiredStorage, long quota, QuotaUpdater quotaUpdater) { 1023 | if (mCustomWebChromeClient != null) { 1024 | mCustomWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); 1025 | } else { 1026 | super.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); 1027 | } 1028 | } 1029 | 1030 | }); 1031 | 1032 | setDownloadListener((url, userAgent, contentDisposition, mimeType, contentLength) -> { 1033 | final String suggestedFilename = URLUtil.guessFileName(url, contentDisposition, mimeType); 1034 | 1035 | if (mListener != null) { 1036 | mListener.onDownloadRequested(url, suggestedFilename, mimeType, contentLength, contentDisposition, userAgent); 1037 | } 1038 | }); 1039 | } 1040 | 1041 | @Override 1042 | public void loadUrl(final String url, Map additionalHttpHeaders) { 1043 | if (additionalHttpHeaders == null) { 1044 | additionalHttpHeaders = mHttpHeaders; 1045 | } else if (mHttpHeaders.size() > 0) { 1046 | additionalHttpHeaders.putAll(mHttpHeaders); 1047 | } 1048 | 1049 | super.loadUrl(url, additionalHttpHeaders); 1050 | } 1051 | 1052 | @Override 1053 | public void loadUrl(final String url) { 1054 | if (mHttpHeaders.size() > 0) { 1055 | super.loadUrl(url, mHttpHeaders); 1056 | } else { 1057 | super.loadUrl(url); 1058 | } 1059 | } 1060 | 1061 | public void loadUrl(String url, final boolean preventCaching) { 1062 | if (preventCaching) { 1063 | url = makeUrlUnique(url); 1064 | } 1065 | 1066 | loadUrl(url); 1067 | } 1068 | 1069 | public void loadUrl(String url, final boolean preventCaching, final Map additionalHttpHeaders) { 1070 | if (preventCaching) { 1071 | url = makeUrlUnique(url); 1072 | } 1073 | 1074 | loadUrl(url, additionalHttpHeaders); 1075 | } 1076 | 1077 | public boolean isPermittedUrl(final String url) { 1078 | // if the permitted hostnames have not been restricted to a specific set 1079 | if (mPermittedHostnames.size() == 0) { 1080 | // all hostnames are allowed 1081 | return true; 1082 | } 1083 | 1084 | final Uri parsedUrl = Uri.parse(url); 1085 | 1086 | // get the hostname of the URL that is to be checked 1087 | final String actualHost = parsedUrl.getHost(); 1088 | 1089 | // if the hostname could not be determined, usually because the URL has been invalid 1090 | if (actualHost == null) { 1091 | return false; 1092 | } 1093 | 1094 | // if the host contains invalid characters (e.g. a backslash) 1095 | if (!actualHost.matches("^[a-zA-Z0-9._!~*')(;:&=+$,%\\[\\]-]*$")) { 1096 | // prevent mismatches between interpretations by `Uri` and `WebView`, e.g. for `http://evil.example.com\.good.example.com/` 1097 | return false; 1098 | } 1099 | 1100 | // get the user information from the authority part of the URL that is to be checked 1101 | final String actualUserInformation = parsedUrl.getUserInfo(); 1102 | 1103 | // if the user information contains invalid characters (e.g. a backslash) 1104 | if (actualUserInformation != null && !actualUserInformation.matches("^[a-zA-Z0-9._!~*')(;:&=+$,%-]*$")) { 1105 | // prevent mismatches between interpretations by `Uri` and `WebView`, e.g. for `http://evil.example.com\@good.example.com/` 1106 | return false; 1107 | } 1108 | 1109 | // for every hostname in the set of permitted hosts 1110 | for (String expectedHost : mPermittedHostnames) { 1111 | // if the two hostnames match or if the actual host is a subdomain of the expected host 1112 | if (actualHost.equals(expectedHost) || actualHost.endsWith("." + expectedHost)) { 1113 | // the actual hostname of the URL to be checked is allowed 1114 | return true; 1115 | } 1116 | } 1117 | 1118 | // the actual hostname of the URL to be checked is not allowed since there were no matches 1119 | return false; 1120 | } 1121 | 1122 | /** 1123 | * @deprecated use `isPermittedUrl` instead 1124 | */ 1125 | protected boolean isHostnameAllowed(final String url) { 1126 | return isPermittedUrl(url); 1127 | } 1128 | 1129 | protected void setLastError() { 1130 | mLastError = System.currentTimeMillis(); 1131 | } 1132 | 1133 | protected boolean hasError() { 1134 | return (mLastError + 500) >= System.currentTimeMillis(); 1135 | } 1136 | 1137 | /** 1138 | * Provides localizations for the 25 most widely spoken languages that have a ISO 639-2/T code 1139 | * 1140 | * @return the label for the file upload prompts as a string 1141 | */ 1142 | protected String getFileUploadPromptLabel() { 1143 | try { 1144 | switch (mLanguageIso3) { 1145 | case "zho": 1146 | return decodeBase64("6YCJ5oup5LiA5Liq5paH5Lu2"); 1147 | case "spa": 1148 | return decodeBase64("RWxpamEgdW4gYXJjaGl2bw=="); 1149 | case "hin": 1150 | return decodeBase64("4KSP4KSVIOCkq+CkvOCkvuCkh+CksiDgpJrgpYHgpKjgpYfgpII="); 1151 | case "ben": 1152 | return decodeBase64("4KaP4KaV4Kaf4Ka/IOCmq+CmvuCmh+CmsiDgpqjgpr/gprDgp43gpqzgpr7gpprgpqg="); 1153 | case "ara": 1154 | return decodeBase64("2KfYrtiq2YrYp9ixINmF2YTZgSDZiNin2K3Yrw=="); 1155 | case "por": 1156 | return decodeBase64("RXNjb2xoYSB1bSBhcnF1aXZv"); 1157 | case "rus": 1158 | return decodeBase64("0JLRi9Cx0LXRgNC40YLQtSDQvtC00LjQvSDRhNCw0LnQuw=="); 1159 | case "jpn": 1160 | return decodeBase64("MeODleOCoeOCpOODq+OCkumBuOaKnuOBl+OBpuOBj+OBoOOBleOBhA=="); 1161 | case "pan": 1162 | return decodeBase64("4KiH4Kmx4KiVIOCoq+CovuCoh+CosiDgqJrgqYHgqKPgqYs="); 1163 | case "deu": 1164 | return decodeBase64("V8OkaGxlIGVpbmUgRGF0ZWk="); 1165 | case "jav": 1166 | return decodeBase64("UGlsaWggc2lqaSBiZXJrYXM="); 1167 | case "msa": 1168 | return decodeBase64("UGlsaWggc2F0dSBmYWls"); 1169 | case "tel": 1170 | return decodeBase64("4LCS4LCVIOCwq+CxhuCxluCwsuCxjeCwqOCxgSDgsI7gsILgsJrgsYHgsJXgsYvgsILgsKHgsL8="); 1171 | case "vie": 1172 | return decodeBase64("Q2jhu41uIG3hu5l0IHThuq1wIHRpbg=="); 1173 | case "kor": 1174 | return decodeBase64("7ZWY64KY7J2YIO2MjOydvOydhCDshKDtg50="); 1175 | case "fra": 1176 | return decodeBase64("Q2hvaXNpc3NleiB1biBmaWNoaWVy"); 1177 | case "mar": 1178 | return decodeBase64("4KSr4KS+4KSH4KSyIOCkqOCkv+CkteCkoeCkvg=="); 1179 | case "tam": 1180 | return decodeBase64("4K6S4K6w4K+BIOCuleCvh+CuvuCuquCvjeCuquCviCDgrqTgr4fgrrDgr43grrXgr4E="); 1181 | case "urd": 1182 | return decodeBase64("2KfbjNqpINmB2KfYptmEINmF24zauiDYs9uSINin2YbYqtiu2KfYqCDaqdix24zaug=="); 1183 | case "fas": 1184 | return decodeBase64("2LHYpyDYp9mG2KrYrtin2Kgg2qnZhtuM2K8g24zaqSDZgdin24zZhA=="); 1185 | case "tur": 1186 | return decodeBase64("QmlyIGRvc3lhIHNlw6dpbg=="); 1187 | case "ita": 1188 | return decodeBase64("U2NlZ2xpIHVuIGZpbGU="); 1189 | case "tha": 1190 | return decodeBase64("4LmA4Lil4Li34Lit4LiB4LmE4Lif4Lil4LmM4Lir4LiZ4Li24LmI4LiH"); 1191 | case "guj": 1192 | return decodeBase64("4KqP4KqVIOCqq+CqvuCqh+CqsuCqqOCrhyDgqqrgqrjgqoLgqqY="); 1193 | } 1194 | } catch (Exception ignored) { 1195 | } 1196 | 1197 | // return English translation by default 1198 | return "Choose a file"; 1199 | } 1200 | 1201 | protected void openFileInput(final ValueCallback fileUploadCallbackSecond, final boolean allowMultiple) { 1202 | if (mFileUploadCallbackFirst != null) { 1203 | mFileUploadCallbackFirst.onReceiveValue(null); 1204 | } 1205 | mFileUploadCallbackFirst = null; 1206 | 1207 | if (mFileUploadCallbackSecond != null) { 1208 | mFileUploadCallbackSecond.onReceiveValue(null); 1209 | } 1210 | mFileUploadCallbackSecond = fileUploadCallbackSecond; 1211 | 1212 | Intent i = new Intent(Intent.ACTION_GET_CONTENT); 1213 | i.addCategory(Intent.CATEGORY_OPENABLE); 1214 | 1215 | if (allowMultiple) { 1216 | if (Build.VERSION.SDK_INT >= 18) { 1217 | i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); 1218 | } 1219 | } 1220 | 1221 | i.setType(mUploadableFileTypes); 1222 | 1223 | if (mFragment != null && mFragment.get() != null) { 1224 | mFragment.get().startActivityForResult(Intent.createChooser(i, getFileUploadPromptLabel()), mRequestCodeFilePicker); 1225 | } else if (mActivity != null && mActivity.get() != null) { 1226 | mActivity.get().startActivityForResult(Intent.createChooser(i, getFileUploadPromptLabel()), mRequestCodeFilePicker); 1227 | } 1228 | } 1229 | 1230 | public interface Listener { 1231 | void onPageStarted(String url, Bitmap favicon); 1232 | 1233 | void onPageFinished(String url); 1234 | 1235 | void onPageError(int errorCode, String description, String failingUrl); 1236 | 1237 | void onDownloadRequested(String url, String suggestedFilename, String mimeType, long contentLength, String contentDisposition, String userAgent); 1238 | 1239 | void onExternalPageRequest(String url); 1240 | } 1241 | 1242 | /** 1243 | * Wrapper for methods related to alternative browsers that have their own rendering engines 1244 | */ 1245 | public static class Browsers { 1246 | 1247 | /** 1248 | * Package name of an alternative browser that is installed on this device 1249 | */ 1250 | private static String mAlternativePackage; 1251 | 1252 | /** 1253 | * Returns whether there is an alternative browser with its own rendering engine currently installed 1254 | * 1255 | * @param context a valid `Context` reference 1256 | * @return whether there is an alternative browser or not 1257 | */ 1258 | public static boolean hasAlternative(final Context context) { 1259 | return getAlternative(context) != null; 1260 | } 1261 | 1262 | /** 1263 | * Returns the package name of an alternative browser with its own rendering engine or `null` 1264 | * 1265 | * @param context a valid `Context` reference 1266 | * @return the package name or `null` 1267 | */ 1268 | public static String getAlternative(final Context context) { 1269 | if (mAlternativePackage != null) { 1270 | return mAlternativePackage; 1271 | } 1272 | 1273 | final List alternativeBrowsers = Arrays.asList(ALTERNATIVE_BROWSERS); 1274 | final List apps = context.getPackageManager().getInstalledApplications(PackageManager.GET_META_DATA); 1275 | 1276 | for (ApplicationInfo app : apps) { 1277 | if (!app.enabled) { 1278 | continue; 1279 | } 1280 | 1281 | if (alternativeBrowsers.contains(app.packageName)) { 1282 | mAlternativePackage = app.packageName; 1283 | 1284 | return app.packageName; 1285 | } 1286 | } 1287 | 1288 | return null; 1289 | } 1290 | 1291 | /** 1292 | * Opens the given URL in an alternative browser 1293 | * 1294 | * @param context a valid `Activity` reference 1295 | * @param url the URL to open 1296 | */ 1297 | public static void openUrl(final Activity context, final String url) { 1298 | openUrl(context, url, false); 1299 | } 1300 | 1301 | /** 1302 | * Opens the given URL in an alternative browser 1303 | * 1304 | * @param context a valid `Activity` reference 1305 | * @param url the URL to open 1306 | * @param withoutTransition whether to switch to the browser `Activity` without a transition 1307 | */ 1308 | public static void openUrl(final Activity context, final String url, final boolean withoutTransition) { 1309 | final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 1310 | intent.setPackage(getAlternative(context)); 1311 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1312 | 1313 | context.startActivity(intent); 1314 | 1315 | if (withoutTransition) { 1316 | context.overridePendingTransition(0, 0); 1317 | } 1318 | } 1319 | 1320 | } 1321 | 1322 | } 1323 | -------------------------------------------------------------------------------- /Source/sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion '29.0.3' 6 | 7 | defaultConfig { 8 | applicationId "im.delight.android.examples.webview" 9 | minSdkVersion 14 10 | targetSdkVersion 28 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | 20 | compileOptions { 21 | encoding "UTF-8" 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | 26 | lintOptions { 27 | disable 'GoogleAppIndexingWarning' 28 | } 29 | 30 | packagingOptions { 31 | exclude 'META-INF/*' 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation project(':library') 37 | } 38 | -------------------------------------------------------------------------------- /Source/sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/sample/src/main/java/im/delight/android/examples/webview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.examples.webview; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.graphics.Bitmap; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.webkit.WebChromeClient; 9 | import android.webkit.WebView; 10 | import android.webkit.WebViewClient; 11 | import android.widget.Toast; 12 | 13 | import im.delight.android.webview.AdvancedWebView; 14 | 15 | public class MainActivity extends Activity implements AdvancedWebView.Listener { 16 | 17 | private static final String TEST_PAGE_URL = "https://www.example.org/"; 18 | private AdvancedWebView mWebView; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_main); 24 | 25 | mWebView = findViewById(R.id.webview); 26 | mWebView.setListener(this, this); 27 | mWebView.setGeolocationEnabled(false); 28 | mWebView.setMixedContentAllowed(false); 29 | mWebView.setCookiesEnabled(true); 30 | mWebView.setThirdPartyCookiesEnabled(true); 31 | mWebView.setWebViewClient(new WebViewClient() { 32 | 33 | @Override 34 | public void onPageFinished(WebView view, String url) { 35 | Toast.makeText(MainActivity.this, "Finished loading", Toast.LENGTH_SHORT).show(); 36 | } 37 | 38 | }); 39 | mWebView.setWebChromeClient(new WebChromeClient() { 40 | 41 | @Override 42 | public void onReceivedTitle(WebView view, String title) { 43 | super.onReceivedTitle(view, title); 44 | Toast.makeText(MainActivity.this, title, Toast.LENGTH_SHORT).show(); 45 | } 46 | 47 | }); 48 | mWebView.addHttpHeader("X-Requested-With", ""); 49 | mWebView.loadUrl(TEST_PAGE_URL); 50 | } 51 | 52 | @Override 53 | protected void onResume() { 54 | super.onResume(); 55 | mWebView.onResume(); 56 | // ... 57 | } 58 | 59 | @Override 60 | protected void onPause() { 61 | mWebView.onPause(); 62 | // ... 63 | super.onPause(); 64 | } 65 | 66 | @Override 67 | protected void onDestroy() { 68 | mWebView.onDestroy(); 69 | // ... 70 | super.onDestroy(); 71 | } 72 | 73 | @Override 74 | protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 75 | super.onActivityResult(requestCode, resultCode, intent); 76 | mWebView.onActivityResult(requestCode, resultCode, intent); 77 | // ... 78 | } 79 | 80 | @Override 81 | public void onBackPressed() { 82 | if (!mWebView.onBackPressed()) { 83 | return; 84 | } 85 | // ... 86 | super.onBackPressed(); 87 | } 88 | 89 | @Override 90 | public void onPageStarted(String url, Bitmap favicon) { 91 | mWebView.setVisibility(View.INVISIBLE); 92 | } 93 | 94 | @Override 95 | public void onPageFinished(String url) { 96 | mWebView.setVisibility(View.VISIBLE); 97 | } 98 | 99 | @Override 100 | public void onPageError(int errorCode, String description, String failingUrl) { 101 | Toast.makeText(MainActivity.this, "onPageError(errorCode = " + errorCode + ", description = " + description + ", failingUrl = " + failingUrl + ")", Toast.LENGTH_SHORT).show(); 102 | } 103 | 104 | @Override 105 | public void onDownloadRequested(String url, String suggestedFilename, String mimeType, long contentLength, String contentDisposition, String userAgent) { 106 | Toast.makeText(MainActivity.this, "onDownloadRequested(url = " + url + ", suggestedFilename = " + suggestedFilename + ", mimeType = " + mimeType + ", contentLength = " + contentLength + ", contentDisposition = " + contentDisposition + ", userAgent = " + userAgent + ")", Toast.LENGTH_LONG).show(); 107 | 108 | /*if (AdvancedWebView.handleDownload(this, url, suggestedFilename)) { 109 | // download successfully handled 110 | } 111 | else { 112 | // download couldn't be handled because user has disabled download manager app on the device 113 | }*/ 114 | } 115 | 116 | @Override 117 | public void onExternalPageRequest(String url) { 118 | Toast.makeText(MainActivity.this, "onExternalPageRequest(url = " + url + ")", Toast.LENGTH_SHORT).show(); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /Source/sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacoTheDank/Android-AdvancedWebView/e461b74d34d6ef099b27a35d38ce5c9058e37d94/Source/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Source/sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacoTheDank/Android-AdvancedWebView/e461b74d34d6ef099b27a35d38ce5c9058e37d94/Source/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Source/sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacoTheDank/Android-AdvancedWebView/e461b74d34d6ef099b27a35d38ce5c9058e37d94/Source/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Source/sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /Source/sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Android-AdvancedWebView-Test 4 | 5 | 6 | -------------------------------------------------------------------------------- /Source/sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Source/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library' 2 | include ':sample' 3 | --------------------------------------------------------------------------------