2024-01-10 18:54:00 +01:00
/ * Copyright ( C ) 2016 - 2024 Andreas Shimokawa , Carsten Pfeiffer , Daniele
Gobbetti , José Rebelo , Taavi Eomäe
2017-10-25 09:29:36 +02:00
This file is part of Gadgetbridge .
Gadgetbridge is free software : you can redistribute it and / or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
Gadgetbridge is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU Affero General Public License for more details .
You should have received a copy of the GNU Affero General Public License
2024-01-10 18:54:00 +01:00
along with this program . If not , see < https : //www.gnu.org/licenses/>. */
2016-12-28 20:53:17 +01:00
package nodomain.freeyourgadget.gadgetbridge.util ;
2017-01-01 18:33:39 +01:00
import android.app.Activity ;
2017-08-05 16:04:48 +02:00
import android.content.ComponentName ;
2016-12-28 20:53:17 +01:00
import android.content.Context ;
import android.content.Intent ;
2017-03-04 20:43:32 +01:00
import android.content.MutableContextWrapper ;
2017-08-05 16:04:48 +02:00
import android.content.ServiceConnection ;
2018-03-17 16:57:02 +01:00
import android.content.pm.PackageManager ;
2017-01-01 21:01:58 +01:00
import android.os.Build ;
2017-08-05 16:04:48 +02:00
import android.os.Bundle ;
2017-03-04 20:43:32 +01:00
import android.os.Handler ;
2017-08-05 16:04:48 +02:00
import android.os.IBinder ;
2017-03-04 20:43:32 +01:00
import android.os.Looper ;
2017-08-05 16:04:48 +02:00
import android.os.Message ;
import android.os.Messenger ;
2018-03-18 00:39:36 +01:00
import android.os.RemoteException ;
2017-01-02 12:07:57 +01:00
import android.webkit.WebResourceResponse ;
2016-12-28 20:53:17 +01:00
import android.webkit.WebSettings ;
import android.webkit.WebView ;
2017-02-27 19:02:34 +01:00
2020-08-26 04:34:41 +02:00
import androidx.annotation.NonNull ;
2016-12-28 20:53:17 +01:00
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
2017-02-27 19:02:34 +01:00
import java.io.ByteArrayInputStream ;
2017-10-13 21:57:22 +02:00
import java.nio.charset.Charset ;
2017-01-03 15:04:51 +01:00
import java.util.HashMap ;
2017-07-26 18:12:12 +02:00
import java.util.Map ;
2016-12-28 20:53:17 +01:00
import java.util.UUID ;
2017-08-05 16:04:48 +02:00
import java.util.concurrent.CountDownLatch ;
2016-12-28 20:53:17 +01:00
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice ;
2017-09-25 17:12:35 +02:00
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBChromeClient ;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBWebClient ;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.JSInterface ;
2016-12-28 20:53:17 +01:00
2017-02-25 21:50:05 +01:00
public class WebViewSingleton {
2016-12-28 20:53:17 +01:00
private static final Logger LOG = LoggerFactory . getLogger ( WebViewSingleton . class ) ;
2020-08-26 04:34:41 +02:00
private static final WebViewSingleton instance = new WebViewSingleton ( ) ;
2016-12-28 20:53:17 +01:00
2018-03-18 00:39:36 +01:00
private WebView webView = null ;
2017-03-04 20:43:32 +01:00
private MutableContextWrapper contextWrapper ;
private Looper mainLooper ;
2018-03-18 00:39:36 +01:00
private UUID currentRunningUUID ;
private Messenger internetHelper = null ;
public boolean internetHelperBound ;
private boolean internetHelperInstalled ;
private CountDownLatch latch ;
private WebResourceResponse internetResponse ;
private Messenger internetHelperListener ;
2016-12-28 20:53:17 +01:00
private WebViewSingleton ( ) {
}
2020-08-26 04:34:41 +02:00
//Internet helper outgoing connection
private final ServiceConnection internetHelperConnection = new ServiceConnection ( ) {
public void onServiceConnected ( ComponentName className , IBinder service ) {
LOG . info ( " internet helper service bound " ) ;
internetHelperBound = true ;
internetHelper = new Messenger ( service ) ;
}
public void onServiceDisconnected ( ComponentName className ) {
LOG . info ( " internet helper service unbound " ) ;
internetHelper = null ;
internetHelperBound = false ;
}
} ;
2017-09-25 23:00:39 +02:00
public static synchronized void ensureCreated ( Activity context ) {
2018-03-18 00:39:36 +01:00
if ( instance . webView = = null ) {
instance . contextWrapper = new MutableContextWrapper ( context ) ;
instance . mainLooper = context . getMainLooper ( ) ;
instance . webView = new WebView ( instance . contextWrapper ) ;
2017-07-26 18:12:12 +02:00
WebView . setWebContentsDebuggingEnabled ( true ) ;
2018-03-18 00:39:36 +01:00
instance . webView . setWillNotDraw ( true ) ;
instance . webView . clearCache ( true ) ;
instance . webView . setWebViewClient ( new GBWebClient ( ) ) ;
instance . webView . setWebChromeClient ( new GBChromeClient ( ) ) ;
WebSettings webSettings = instance . webView . getSettings ( ) ;
2020-08-26 04:28:03 +02:00
//noinspection SetJavaScriptEnabled
2017-02-25 18:01:08 +01:00
webSettings . setJavaScriptEnabled ( true ) ;
//needed to access the DOM
webSettings . setDomStorageEnabled ( true ) ;
//needed for localstorage
webSettings . setDatabaseEnabled ( true ) ;
2023-12-26 17:48:59 +01:00
// #3373 #3424 - Fix configuration for pebble apps
// TODO: this should use a WebViewAssetLoader
webSettings . setAllowFileAccess ( true ) ;
2017-02-25 18:01:08 +01:00
}
}
2018-03-18 00:39:36 +01:00
public static WebViewSingleton getInstance ( ) {
return instance ;
}
public WebResourceResponse send ( Message webRequest ) throws RemoteException , InterruptedException {
webRequest . replyTo = internetHelperListener ;
latch = new CountDownLatch ( 1 ) ; //the messenger should run on a single thread, hence we don't need to be worried about concurrency. This approach however is certainly not ideal.
internetHelper . send ( webRequest ) ;
latch . await ( ) ;
return internetResponse ;
}
2017-08-05 16:04:48 +02:00
//Internet helper inbound (responses) handler
2018-03-18 00:39:36 +01:00
private class IncomingHandler extends Handler {
2017-10-13 21:57:22 +02:00
private String getCharsetFromHeaders ( String contentType ) {
if ( contentType ! = null & & contentType . toLowerCase ( ) . trim ( ) . contains ( " charset= " ) ) {
String [ ] parts = contentType . toLowerCase ( ) . trim ( ) . split ( " = " ) ;
if ( parts . length > 0 )
return parts [ 1 ] ;
}
return null ;
}
2017-08-05 16:04:48 +02:00
@Override
public void handleMessage ( Message msg ) {
Bundle data = msg . getData ( ) ;
LOG . debug ( " WEBVIEW: internet helper returned: " + data . getString ( " response " ) ) ;
Map < String , String > headers = new HashMap < > ( ) ;
headers . put ( " Access-Control-Allow-Origin " , " * " ) ;
2022-09-09 19:58:34 +02:00
internetResponse = new WebResourceResponse ( data . getString ( " content-type " ) , data . getString ( " content-encoding " ) , 200 , " OK " ,
headers ,
new ByteArrayInputStream ( data . getString ( " response " ) . getBytes ( Charset . forName ( getCharsetFromHeaders ( data . getString ( " content-type " ) ) ) ) )
) ;
2017-08-05 16:04:48 +02:00
latch . countDown ( ) ;
}
}
2017-02-25 18:01:08 +01:00
@NonNull
2018-03-18 00:39:36 +01:00
public WebView getWebView ( Context context ) {
contextWrapper . setBaseContext ( context ) ;
return webView ;
2017-02-25 18:01:08 +01:00
}
2017-02-25 15:44:37 +01:00
2018-01-13 21:43:08 +01:00
/ * *
* Checks that the webview is up and the given app is running .
* @param uuid the uuid of the application expected to be running
* @throws IllegalStateException when the webview is not active or the app is not running
* /
2018-03-18 00:39:36 +01:00
public void checkAppRunning ( @NonNull UUID uuid ) {
if ( webView = = null ) {
throw new IllegalStateException ( " instance.webView is null! " ) ;
2018-01-13 21:43:08 +01:00
}
if ( ! uuid . equals ( currentRunningUUID ) ) {
throw new IllegalStateException ( " Expected app " + uuid + " is not running, but " + currentRunningUUID + " is. " ) ;
}
}
2018-03-18 00:39:36 +01:00
public void runJavascriptInterface ( @NonNull Activity context , @NonNull GBDevice device , @NonNull UUID uuid ) {
ensureCreated ( context ) ;
2018-03-17 18:50:53 +01:00
runJavascriptInterface ( device , uuid ) ;
}
2018-03-18 00:39:36 +01:00
public void runJavascriptInterface ( @NonNull GBDevice device , @NonNull UUID uuid ) {
2017-03-04 19:46:18 +01:00
if ( uuid . equals ( currentRunningUUID ) ) {
LOG . debug ( " WEBVIEW uuid not changed keeping the old context " ) ;
} else {
2017-07-24 23:57:07 +02:00
final JSInterface jsInterface = new JSInterface ( device , uuid ) ;
2017-03-04 19:46:18 +01:00
LOG . debug ( " WEBVIEW uuid changed, restarting " ) ;
currentRunningUUID = uuid ;
2018-01-13 21:43:08 +01:00
invokeWebview ( new WebViewRunnable ( ) {
2017-03-04 19:46:18 +01:00
@Override
2018-01-13 21:43:08 +01:00
public void invoke ( WebView webView ) {
webView . onResume ( ) ;
webView . removeJavascriptInterface ( " GBjs " ) ;
webView . addJavascriptInterface ( jsInterface , " GBjs " ) ;
webView . loadUrl ( " file:///android_asset/app_config/configure.html?rand= " + Math . random ( ) * 500 ) ;
2017-03-04 19:46:18 +01:00
}
} ) ;
2018-06-03 18:34:21 +02:00
if ( contextWrapper ! = null & & ! internetHelperBound & & ! internetHelperInstalled ) {
2018-03-17 16:57:02 +01:00
String internetHelperPkg = " nodomain.freeyourgadget.internethelper " ;
String internetHelperCls = internetHelperPkg + " .MyService " ;
try {
2018-03-18 00:39:36 +01:00
contextWrapper . getPackageManager ( ) . getApplicationInfo ( internetHelperPkg , 0 ) ;
2018-03-17 16:57:02 +01:00
Intent intent = new Intent ( ) ;
intent . setComponent ( new ComponentName ( internetHelperPkg , internetHelperCls ) ) ;
2018-03-18 00:39:36 +01:00
contextWrapper . getApplicationContext ( ) . bindService ( intent , internetHelperConnection , Context . BIND_AUTO_CREATE ) ;
2018-03-17 16:57:02 +01:00
internetHelperListener = new Messenger ( new IncomingHandler ( ) ) ;
internetHelperInstalled = true ;
}
catch ( PackageManager . NameNotFoundException e ) {
internetHelperInstalled = false ;
LOG . info ( " WEBVIEW: Internet helper not installed, only mimicked HTTP requests will work. " ) ;
}
2017-09-03 17:55:00 +02:00
}
2017-03-04 19:46:18 +01:00
}
2016-12-28 20:53:17 +01:00
}
2018-03-18 00:39:36 +01:00
public void stopJavascriptInterface ( ) {
2018-01-13 21:43:08 +01:00
invokeWebview ( new WebViewRunnable ( ) {
2017-10-23 20:47:30 +02:00
@Override
2018-01-13 21:43:08 +01:00
public void invoke ( WebView webView ) {
webView . removeJavascriptInterface ( " GBjs " ) ;
webView . loadUrl ( " about:blank " ) ;
2017-10-23 20:47:30 +02:00
}
} ) ;
}
2018-03-18 00:39:36 +01:00
public void disposeWebView ( ) {
2017-08-05 16:04:48 +02:00
if ( internetHelperBound ) {
LOG . debug ( " WEBVIEW: will unbind the internet helper " ) ;
2018-03-18 00:39:36 +01:00
contextWrapper . getApplicationContext ( ) . unbindService ( internetHelperConnection ) ;
2017-09-03 17:55:00 +02:00
internetHelperBound = false ;
2017-08-05 16:04:48 +02:00
}
2017-09-03 17:55:00 +02:00
currentRunningUUID = null ;
2018-01-13 21:43:08 +01:00
invokeWebview ( new WebViewRunnable ( ) {
2017-01-01 21:01:58 +01:00
@Override
2018-01-13 21:43:08 +01:00
public void invoke ( WebView webView ) {
webView . removeJavascriptInterface ( " GBjs " ) ;
// webView.setWebChromeClient(null);
// webView.setWebViewClient(null);
webView . clearHistory ( ) ;
webView . clearCache ( true ) ;
webView . loadUrl ( " about:blank " ) ;
// webView.freeMemory();
webView . pauseTimers ( ) ;
2018-03-18 00:39:36 +01:00
// webView.destroy();
// webView = null;
2017-02-26 17:57:26 +01:00
// contextWrapper = null;
2017-02-28 21:11:26 +01:00
// jsInterface = null;
2017-01-01 21:01:58 +01:00
}
} ) ;
2016-12-28 20:53:17 +01:00
}
2018-03-18 00:39:36 +01:00
public void invokeWebview ( final WebViewRunnable runnable ) {
if ( webView = = null | | mainLooper = = null ) {
2018-01-13 21:43:08 +01:00
LOG . warn ( " Webview already disposed, ignoring runnable " ) ;
return ;
2017-02-28 21:11:26 +01:00
}
2018-03-18 00:39:36 +01:00
new Handler ( mainLooper ) . post ( new Runnable ( ) {
2018-01-13 21:43:08 +01:00
@Override
public void run ( ) {
2018-03-18 00:39:36 +01:00
if ( webView = = null ) {
2018-01-13 21:43:08 +01:00
LOG . warn ( " Webview already disposed, cannot invoke runnable " ) ;
return ;
2017-02-28 21:11:26 +01:00
}
2018-03-18 00:39:36 +01:00
runnable . invoke ( webView ) ;
2017-02-28 21:11:26 +01:00
}
2018-01-13 21:43:08 +01:00
} ) ;
2017-02-28 21:11:26 +01:00
}
2018-01-13 21:43:08 +01:00
public interface WebViewRunnable {
/ * *
2018-03-18 00:39:36 +01:00
* Called in the main thread with a non - null webView webView
2018-01-13 21:43:08 +01:00
* @param webView the webview , never null
* /
void invoke ( WebView webView ) ;
}
2016-12-28 20:53:17 +01:00
}