diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java | 1054 |
1 files changed, 1054 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java new file mode 100644 index 0000000000..890d78c8b1 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java @@ -0,0 +1,1054 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * vim: ts=4 sw=4 expandtab: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.provider.Settings; +import android.text.format.DateFormat; +import android.util.Log; +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringDef; +import androidx.annotation.UiThread; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.lifecycle.ProcessLifecycleOwner; +import java.io.File; +import java.io.FileNotFoundException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoNetworkManager; +import org.mozilla.gecko.GeckoScreenChangeListener; +import org.mozilla.gecko.GeckoScreenOrientation; +import org.mozilla.gecko.GeckoScreenOrientation.ScreenOrientation; +import org.mozilla.gecko.GeckoSystemStateListener; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.process.MemoryController; +import org.mozilla.gecko.util.BundleEventListener; +import org.mozilla.gecko.util.DebugConfig; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.GeckoBundle; +import org.mozilla.gecko.util.ThreadUtils; + +public final class GeckoRuntime implements Parcelable { + private static final String LOGTAG = "GeckoRuntime"; + private static final boolean DEBUG = false; + + private static final String CONFIG_FILE_PATH_TEMPLATE = + "/data/local/tmp/%s-geckoview-config.yaml"; + + /** + * Intent action sent to the crash handler when a crash is encountered. + * + * @see GeckoRuntimeSettings.Builder#crashHandler(Class) + */ + public static final String ACTION_CRASHED = "org.mozilla.gecko.ACTION_CRASHED"; + + /** + * This is a key for extra data sent with {@link #ACTION_CRASHED}. It refers to a String with the + * path to a Breakpad minidump file containing information about the crash. Several crash + * reporters are able to ingest this in a crash report, including <a + * href="https://sentry.io">Sentry</a> and Mozilla's <a + * href="https://wiki.mozilla.org/Socorro">Socorro</a>. <br> + * <br> + * Be aware, the minidump can contain personally identifiable information. Ensure you are obeying + * all applicable laws and policies before sending this to a remote server. + * + * @see GeckoRuntimeSettings.Builder#crashHandler(Class) + */ + public static final String EXTRA_MINIDUMP_PATH = "minidumpPath"; + + /** + * This is a key for extra data sent with {@link #ACTION_CRASHED}. It refers to a string with the + * path to a file containing extra metadata about the crash. The file contains key-value pairs in + * the form + * + * <pre>Key=Value</pre> + * + * Be aware, it may contain sensitive data such as the URI that was loaded at the time of the + * crash. + */ + public static final String EXTRA_EXTRAS_PATH = "extrasPath"; + + /** + * This is a key for extra data sent with {@link #ACTION_CRASHED}. The value is a String matching + * one of the `CRASHED_PROCESS_TYPE_*` constants, describing what type of process the crash + * occurred in. + * + * @see GeckoSession.ContentDelegate#onCrash(GeckoSession) + */ + public static final String EXTRA_CRASH_PROCESS_TYPE = "processType"; + + /** + * Value for {@link #EXTRA_CRASH_PROCESS_TYPE} indicating the main application process was + * affected by the crash, which is therefore fatal. + */ + public static final String CRASHED_PROCESS_TYPE_MAIN = "MAIN"; + + /** + * Value for {@link #EXTRA_CRASH_PROCESS_TYPE} indicating a foreground child process, such as a + * content process, crashed. The application may be able to recover from this crash, but it was + * likely noticable to the user. + */ + public static final String CRASHED_PROCESS_TYPE_FOREGROUND_CHILD = "FOREGROUND_CHILD"; + + /** + * Value for {@link #EXTRA_CRASH_PROCESS_TYPE} indicating a background child process crashed. This + * should have been recovered from automatically, and will have had minimal impact to the user, if + * any. + */ + public static final String CRASHED_PROCESS_TYPE_BACKGROUND_CHILD = "BACKGROUND_CHILD"; + + private final MemoryController mMemoryController = new MemoryController(); + + @Retention(RetentionPolicy.SOURCE) + @StringDef( + value = { + CRASHED_PROCESS_TYPE_MAIN, + CRASHED_PROCESS_TYPE_FOREGROUND_CHILD, + CRASHED_PROCESS_TYPE_BACKGROUND_CHILD + }) + public @interface CrashedProcessType {} + + private final class LifecycleListener implements LifecycleObserver { + private boolean mPaused = false; + + public LifecycleListener() {} + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + void onCreate() { + Log.d(LOGTAG, "Lifecycle: onCreate"); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + void onStart() { + Log.d(LOGTAG, "Lifecycle: onStart"); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + void onResume() { + Log.d(LOGTAG, "Lifecycle: onResume"); + if (mPaused) { + // Do not trigger the first onResume event because it breaks nsAppShell::sPauseCount counter + // thresholds. + GeckoThread.onResume(); + } + mPaused = false; + // Can resume location services, checks if was in use before going to background + GeckoAppShell.resumeLocation(); + // Monitor network status and send change notifications to Gecko + // while active. + GeckoNetworkManager.getInstance().start(GeckoAppShell.getApplicationContext()); + + // Set settings that may have changed between last app opening + GeckoAppShell.setIs24HourFormat( + DateFormat.is24HourFormat(GeckoAppShell.getApplicationContext())); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + void onPause() { + Log.d(LOGTAG, "Lifecycle: onPause"); + mPaused = true; + // Pause listening for locations when in background + GeckoAppShell.pauseLocation(); + // Stop monitoring network status while inactive. + GeckoNetworkManager.getInstance().stop(); + GeckoThread.onPause(); + } + } + + private static GeckoRuntime sDefaultRuntime; + + /** + * Get the default runtime for the given context. This will create and initialize the runtime with + * the default settings. + * + * <p>Note: Only use this for session-less apps. For regular apps, use create() instead. + * + * @param context An application context for the default runtime. + * @return The (static) default runtime for the context. + */ + @UiThread + public static synchronized @NonNull GeckoRuntime getDefault(final @NonNull Context context) { + ThreadUtils.assertOnUiThread(); + if (DEBUG) { + Log.d(LOGTAG, "getDefault"); + } + if (sDefaultRuntime == null) { + sDefaultRuntime = new GeckoRuntime(); + sDefaultRuntime.attachTo(context); + sDefaultRuntime.init(context, new GeckoRuntimeSettings()); + } + + return sDefaultRuntime; + } + + private static GeckoRuntime sRuntime; + private GeckoRuntimeSettings mSettings; + private Delegate mDelegate; + private ServiceWorkerDelegate mServiceWorkerDelegate; + private WebNotificationDelegate mNotificationDelegate; + private ActivityDelegate mActivityDelegate; + private OrientationController mOrientationController; + private StorageController mStorageController; + private final WebExtensionController mWebExtensionController; + private WebPushController mPushController; + private final ContentBlockingController mContentBlockingController; + private final Autocomplete.StorageProxy mAutocompleteStorageProxy; + private final ProfilerController mProfilerController; + private final GeckoScreenChangeListener mScreenChangeListener; + + private GeckoRuntime() { + mWebExtensionController = new WebExtensionController(this); + mContentBlockingController = new ContentBlockingController(); + mAutocompleteStorageProxy = new Autocomplete.StorageProxy(); + mProfilerController = new ProfilerController(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + mScreenChangeListener = new GeckoScreenChangeListener(); + } else { + mScreenChangeListener = null; + } + + if (sRuntime != null) { + throw new IllegalStateException("Only one GeckoRuntime instance is allowed"); + } + sRuntime = this; + } + + @WrapForJNI + @UiThread + /* package */ @Nullable + static GeckoRuntime getInstance() { + return sRuntime; + } + + /** + * Called by mozilla::dom::ClientOpenWindow to retrieve the window id to use for a + * ServiceWorkerClients.openWindow() request. + * + * @param url validated Url being requested to be opened in a new window. + * @return SessionID to use for the request. + */ + @SuppressLint("WrongThread") // for .isOpen() which is called on the UI thread + @WrapForJNI(calledFrom = "gecko") + private static @NonNull GeckoResult<String> serviceWorkerOpenWindow(final @NonNull String url) { + if (sRuntime != null && sRuntime.mServiceWorkerDelegate != null) { + final GeckoResult<String> result = new GeckoResult<>(); + // perform the onOpenWindow call in the UI thread + ThreadUtils.runOnUiThread( + () -> { + sRuntime + .mServiceWorkerDelegate + .onOpenWindow(url) + .accept( + session -> { + if (session != null) { + if (!session.isOpen()) { + session.open(sRuntime); + } + result.complete(session.getId()); + } else { + result.complete(null); + } + }); + }); + return result; + } else { + return GeckoResult.fromException( + new java.lang.RuntimeException("No available Service Worker delegate.")); + } + } + + /** + * Attach the runtime to the given context. + * + * @param context The new context to attach to. + */ + @UiThread + public void attachTo(final @NonNull Context context) { + ThreadUtils.assertOnUiThread(); + if (DEBUG) { + Log.d(LOGTAG, "attachTo " + context.getApplicationContext()); + } + final Context appContext = context.getApplicationContext(); + if (!appContext.equals(GeckoAppShell.getApplicationContext())) { + GeckoAppShell.setApplicationContext(appContext); + } + } + + private final BundleEventListener mEventListener = + new BundleEventListener() { + @Override + public void handleMessage( + final String event, final GeckoBundle message, final EventCallback callback) { + final Class<?> crashHandler = GeckoRuntime.this.getSettings().mCrashHandler; + + if ("Gecko:Exited".equals(event) && mDelegate != null) { + mDelegate.onShutdown(); + EventDispatcher.getInstance() + .unregisterUiThreadListener(mEventListener, "Gecko:Exited"); + } else if ("GeckoView:Test:NewTab".equals(event)) { + final String url = message.getString("url", "about:blank"); + serviceWorkerOpenWindow(url) + .then( + (GeckoResult.OnValueListener<String, Void>) + value -> { + callback.sendSuccess(value); + return null; + }) + .exceptionally( + (GeckoResult.OnExceptionListener<Void>) + error -> { + callback.sendError(error + " Could not open tab."); + return null; + }); + } else if ("GeckoView:ChildCrashReport".equals(event) && crashHandler != null) { + final Context context = GeckoAppShell.getApplicationContext(); + final Intent i = new Intent(ACTION_CRASHED, null, context, crashHandler); + i.putExtra(EXTRA_MINIDUMP_PATH, message.getString(EXTRA_MINIDUMP_PATH)); + i.putExtra(EXTRA_EXTRAS_PATH, message.getString(EXTRA_EXTRAS_PATH)); + i.putExtra(EXTRA_CRASH_PROCESS_TYPE, message.getString(EXTRA_CRASH_PROCESS_TYPE)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(i); + } else { + context.startService(i); + } + } + } + }; + + private static String getProcessName(final Context context) { + final ActivityManager manager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + final List<ActivityManager.RunningAppProcessInfo> infos = manager.getRunningAppProcesses(); + if (infos == null) { + return null; + } + for (final ActivityManager.RunningAppProcessInfo info : infos) { + if (info.pid == Process.myPid()) { + return info.processName; + } + } + + return null; + } + + /* package */ boolean init( + final @NonNull Context context, final @NonNull GeckoRuntimeSettings settings) { + if (DEBUG) { + Log.d(LOGTAG, "init"); + } + int flags = GeckoThread.FLAG_PRELOAD_CHILD; + + if (settings.getPauseForDebuggerEnabled()) { + flags |= GeckoThread.FLAG_DEBUGGING; + } + + final Class<?> crashHandler = settings.getCrashHandler(); + if (crashHandler != null) { + try { + final ServiceInfo info = + context.getPackageManager().getServiceInfo(new ComponentName(context, crashHandler), 0); + if (info.processName.equals(getProcessName(context))) { + throw new IllegalArgumentException( + "Crash handler service must run in a separate process"); + } + + EventDispatcher.getInstance() + .registerUiThreadListener(mEventListener, "GeckoView:ChildCrashReport"); + + flags |= GeckoThread.FLAG_ENABLE_NATIVE_CRASHREPORTER; + } catch (final PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("Crash handler must be registered as a service"); + } + } + + GeckoAppShell.useMaxScreenDepth(settings.getUseMaxScreenDepth()); + GeckoAppShell.setDisplayDensityOverride(settings.getDisplayDensityOverride()); + GeckoAppShell.setDisplayDpiOverride(settings.getDisplayDpiOverride()); + GeckoAppShell.setScreenSizeOverride(settings.getScreenSizeOverride()); + GeckoAppShell.setCrashHandlerService(settings.getCrashHandler()); + GeckoFontScaleListener.getInstance().attachToContext(context, settings); + + Bundle extras = settings.getExtras(); + String[] args = settings.getArguments(); + Map<String, Object> prefs = settings.getPrefsMap(); + + // Older versions have problems with SnakeYaml + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String configFilePath = settings.getConfigFilePath(); + if (configFilePath == null) { + // Default to /data/local/tmp/$PACKAGE-geckoview-config.yaml if android:debuggable="true" + // or if this application is the current Android "debug_app", and to not read configuration + // from a file otherwise. + if (isApplicationDebuggable(context) || isApplicationCurrentDebugApp(context)) { + configFilePath = + String.format(CONFIG_FILE_PATH_TEMPLATE, context.getApplicationInfo().packageName); + } + } + + if (configFilePath != null && !configFilePath.isEmpty()) { + try { + final DebugConfig debugConfig = DebugConfig.fromFile(new File(configFilePath)); + Log.i(LOGTAG, "Adding debug configuration from: " + configFilePath); + prefs = debugConfig.mergeIntoPrefs(prefs); + args = debugConfig.mergeIntoArgs(args); + extras = debugConfig.mergeIntoExtras(extras); + } catch (final DebugConfig.ConfigException e) { + Log.w(LOGTAG, "Failed to add debug configuration from: " + configFilePath, e); + } catch (final FileNotFoundException e) { + } + } + } + + final GeckoThread.InitInfo info = + GeckoThread.InitInfo.builder() + .args(args) + .extras(extras) + .flags(flags) + .prefs(prefs) + .outFilePath(extras != null ? extras.getString("out_file") : null) + .build(); + + if (info.xpcshell + && !"org.mozilla.geckoview.test_runner" + .equals(context.getApplicationContext().getPackageName())) { + throw new IllegalArgumentException("Only the test app can run -xpcshell."); + } + + if (info.xpcshell) { + // Xpcshell tests need multi-e10s to work properly + settings.setProcessCount(BuildConfig.MOZ_ANDROID_CONTENT_SERVICE_COUNT); + } + + if (!GeckoThread.init(info)) { + Log.w(LOGTAG, "init failed (could not initiate GeckoThread)"); + return false; + } + + if (!GeckoThread.launch()) { + Log.w(LOGTAG, "init failed (GeckoThread already launched)"); + return false; + } + + mSettings = settings; + + // Bug 1453062 -- the EventDispatcher should really live here (or in GeckoThread) + EventDispatcher.getInstance() + .registerUiThreadListener(mEventListener, "Gecko:Exited", "GeckoView:Test:NewTab"); + + // Attach and commit settings. + mSettings.attachTo(this); + + // Initialize the system ClipboardManager by accessing it on the main thread. + GeckoAppShell.getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE); + + // Add process lifecycle listener to react to backgrounding events. + ProcessLifecycleOwner.get().getLifecycle().addObserver(new LifecycleListener()); + + // Add Display Manager listener to listen screen orientation change. + if (mScreenChangeListener != null) { + mScreenChangeListener.enable(); + } + + mProfilerController.addMarker( + "GeckoView Initialization START", mProfilerController.getProfilerTime()); + return true; + } + + private boolean isApplicationDebuggable(final @NonNull Context context) { + final ApplicationInfo applicationInfo = context.getApplicationInfo(); + return (applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } + + private boolean isApplicationCurrentDebugApp(final @NonNull Context context) { + final ApplicationInfo applicationInfo = context.getApplicationInfo(); + + final String currentDebugApp; + if (Build.VERSION.SDK_INT >= 17) { + currentDebugApp = + Settings.Global.getString(context.getContentResolver(), Settings.Global.DEBUG_APP); + } else { + currentDebugApp = + Settings.System.getString(context.getContentResolver(), Settings.System.DEBUG_APP); + } + return applicationInfo.packageName.equals(currentDebugApp); + } + + /* package */ void setDefaultPrefs(final GeckoBundle prefs) { + EventDispatcher.getInstance().dispatch("GeckoView:SetDefaultPrefs", prefs); + } + + /** + * Create a new runtime with default settings and attach it to the given context. + * + * <p>Create will throw if there is already an active Gecko instance running, to prevent that, + * bind the runtime to the process lifetime instead of the activity lifetime. + * + * @param context The context of the runtime. + * @return An initialized runtime. + */ + @UiThread + public static @NonNull GeckoRuntime create(final @NonNull Context context) { + ThreadUtils.assertOnUiThread(); + return create(context, new GeckoRuntimeSettings()); + } + + /** + * Returns a WebExtensionController for this GeckoRuntime. + * + * @return an instance of {@link WebExtensionController}. + */ + @UiThread + public @NonNull WebExtensionController getWebExtensionController() { + return mWebExtensionController; + } + + /** + * Returns the ContentBlockingController for this GeckoRuntime. + * + * @return An instance of {@link ContentBlockingController}. + */ + @UiThread + public @NonNull ContentBlockingController getContentBlockingController() { + return mContentBlockingController; + } + + /** + * Returns a ProfilerController for this GeckoRuntime. + * + * @return an instance of {@link ProfilerController}. + */ + @UiThread + public @NonNull ProfilerController getProfilerController() { + return mProfilerController; + } + + /** + * Create a new runtime with the given settings and attach it to the given context. + * + * <p>Create will throw if there is already an active Gecko instance running, to prevent that, + * bind the runtime to the process lifetime instead of the activity lifetime. + * + * @param context The context of the runtime. + * @param settings The settings for the runtime. + * @return An initialized runtime. + */ + @UiThread + public static @NonNull GeckoRuntime create( + final @NonNull Context context, final @NonNull GeckoRuntimeSettings settings) { + ThreadUtils.assertOnUiThread(); + if (DEBUG) { + Log.d(LOGTAG, "create " + context); + } + + final GeckoRuntime runtime = new GeckoRuntime(); + runtime.attachTo(context); + + if (!runtime.init(context, settings)) { + throw new IllegalStateException("Failed to initialize GeckoRuntime"); + } + + context.registerComponentCallbacks(runtime.mMemoryController); + + return runtime; + } + + /** Shutdown the runtime. This will invalidate all attached sessions. */ + @AnyThread + public void shutdown() { + if (DEBUG) { + Log.d(LOGTAG, "shutdown"); + } + + GeckoSystemStateListener.getInstance().shutdown(); + + if (mScreenChangeListener != null) { + mScreenChangeListener.disable(); + } + + GeckoThread.forceQuit(); + } + + public interface Delegate { + /** + * This is called when the runtime shuts down. Any GeckoSession instances that were opened with + * this instance are now considered closed. + */ + @UiThread + void onShutdown(); + } + + /** + * Set a delegate for receiving callbacks relevant to to this GeckoRuntime. + * + * @param delegate an implementation of {@link GeckoRuntime.Delegate}. + */ + @UiThread + public void setDelegate(final @Nullable Delegate delegate) { + ThreadUtils.assertOnUiThread(); + mDelegate = delegate; + } + + /** + * Returns the current delegate, if any. + * + * @return an instance of {@link GeckoRuntime.Delegate} or null if no delegate has been set. + */ + @UiThread + public @Nullable Delegate getDelegate() { + return mDelegate; + } + + /** + * Set the {@link Autocomplete.StorageDelegate} instance on this runtime. This delegate is + * required for handling autocomplete storage requests. + * + * @param delegate The {@link Autocomplete.StorageDelegate} handling autocomplete storage + * requests. + */ + @UiThread + public void setAutocompleteStorageDelegate( + final @Nullable Autocomplete.StorageDelegate delegate) { + ThreadUtils.assertOnUiThread(); + mAutocompleteStorageProxy.setDelegate(delegate); + } + + /** + * Get the {@link Autocomplete.StorageDelegate} instance set on this runtime. + * + * @return The {@link Autocomplete.StorageDelegate} set on this runtime. + */ + @UiThread + public @Nullable Autocomplete.StorageDelegate getAutocompleteStorageDelegate() { + ThreadUtils.assertOnUiThread(); + return mAutocompleteStorageProxy.getDelegate(); + } + + @UiThread + public interface ServiceWorkerDelegate { + + /** + * This is called when a service worker tries to open a new window using client.openWindow() The + * GeckoView application should provide an open {@link GeckoSession} to open the url. + * + * @param url Url which the Service Worker wishes to open in a new window. + * @return New or existing open {@link GeckoSession} in which to open the requested url. + * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service + * Worker API</a> + * @see <a + * href="https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow">openWindow()</a> + */ + @UiThread + @NonNull + GeckoResult<GeckoSession> onOpenWindow(@NonNull String url); + } + + /** + * Sets the {@link ServiceWorkerDelegate} to be used for Service Worker requests. + * + * @param serviceWorkerDelegate An instance of {@link ServiceWorkerDelegate}. + * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service + * Worker API</a> + */ + @UiThread + public void setServiceWorkerDelegate( + final @Nullable ServiceWorkerDelegate serviceWorkerDelegate) { + mServiceWorkerDelegate = serviceWorkerDelegate; + } + + /** + * Gets the {@link ServiceWorkerDelegate} to be used for Service Worker requests. + * + * @return the {@link ServiceWorkerDelegate} instance set by {@link #setServiceWorkerDelegate} + */ + @UiThread + @Nullable + public ServiceWorkerDelegate getServiceWorkerDelegate() { + return mServiceWorkerDelegate; + } + + /** + * Sets the delegate to be used for handling Web Notifications. + * + * @param delegate An instance of {@link WebNotificationDelegate}. + * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification">Web + * Notifications</a> + */ + @UiThread + public void setWebNotificationDelegate(final @Nullable WebNotificationDelegate delegate) { + mNotificationDelegate = delegate; + } + + @WrapForJNI + /* package */ float textScaleFactor() { + return getSettings().getFontSizeFactor(); + } + + @WrapForJNI + /* package */ boolean usesDarkTheme() { + switch (getSettings().getPreferredColorScheme()) { + case GeckoRuntimeSettings.COLOR_SCHEME_SYSTEM: + return GeckoSystemStateListener.getInstance().isNightMode(); + case GeckoRuntimeSettings.COLOR_SCHEME_DARK: + return true; + case GeckoRuntimeSettings.COLOR_SCHEME_LIGHT: + default: + return false; + } + } + + /** + * Returns the current WebNotificationDelegate, if any + * + * @return an instance of WebNotificationDelegate or null if no delegate has been set + */ + @WrapForJNI + @UiThread + public @Nullable WebNotificationDelegate getWebNotificationDelegate() { + return mNotificationDelegate; + } + + @WrapForJNI + @AnyThread + private void notifyOnShow(final WebNotification notification) { + ThreadUtils.runOnUiThread( + () -> { + if (mNotificationDelegate != null) { + mNotificationDelegate.onShowNotification(notification); + } + }); + } + + @WrapForJNI + @AnyThread + private void notifyOnClose(final WebNotification notification) { + ThreadUtils.runOnUiThread( + () -> { + if (mNotificationDelegate != null) { + mNotificationDelegate.onCloseNotification(notification); + } + }); + } + + /** + * This is used to allow GeckoRuntime to start activities via the embedding application (and + * {@link android.app.Activity}). Currently this is used to invoke the Google Play FIDO Activity + * in order to integrate with the Web Authentication API. + * + * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API">Web + * Authentication API</a> + */ + public interface ActivityDelegate { + /** + * Sometimes GeckoView needs the application to perform a {@link + * android.app.Activity#startActivityForResult(Intent, int)} on its behalf. Implementations of + * this method should call that based on the information in the passed {@link PendingIntent}, + * collect the result, and resolve the returned {@link GeckoResult} with that data. If the + * Activity does not return {@link android.app.Activity#RESULT_OK}, the {@link GeckoResult} must + * be completed with an exception of your choosing. + * + * @param intent The {@link PendingIntent} to launch + * @return A {@link GeckoResult} that is eventually resolved with the Activity result. + */ + @UiThread + @Nullable + GeckoResult<Intent> onStartActivityForResult(@NonNull PendingIntent intent); + } + + /** + * Set the {@link ActivityDelegate} instance on this runtime. This delegate is used to provide + * GeckoView support for launching external activities and receiving results from those + * activities. + * + * @param delegate The {@link ActivityDelegate} handling intent launching requests. + */ + @UiThread + public void setActivityDelegate(final @Nullable ActivityDelegate delegate) { + ThreadUtils.assertOnUiThread(); + mActivityDelegate = delegate; + } + + /** + * Get the {@link ActivityDelegate} instance set on this runtime, if any, + * + * @return The {@link ActivityDelegate} set on this runtime. + */ + @UiThread + public @Nullable ActivityDelegate getActivityDelegate() { + ThreadUtils.assertOnUiThread(); + return mActivityDelegate; + } + + @AnyThread + /* package */ GeckoResult<Intent> startActivityForResult(final @NonNull PendingIntent intent) { + if (!ThreadUtils.isOnUiThread()) { + // Delegates expect to be called on the UI thread. + final GeckoResult<Intent> result = new GeckoResult<>(); + + ThreadUtils.runOnUiThread( + () -> { + final GeckoResult<Intent> delegateResult = startActivityForResult(intent); + if (delegateResult != null) { + delegateResult.accept( + val -> result.complete(val), e -> result.completeExceptionally(e)); + } else { + result.completeExceptionally(new IllegalStateException("No result")); + } + }); + + return result; + } + + if (mActivityDelegate == null) { + return GeckoResult.fromException(new IllegalStateException("No delegate attached")); + } + + @SuppressLint("WrongThread") + GeckoResult<Intent> result = mActivityDelegate.onStartActivityForResult(intent); + if (result == null) { + result = GeckoResult.fromException(new IllegalStateException("No result")); + } + + return result; + } + + @AnyThread + @SuppressWarnings("checkstyle:javadocmethod") + public @NonNull GeckoRuntimeSettings getSettings() { + return mSettings; + } + + /** Notify Gecko that the screen orientation has changed. */ + @UiThread + public void orientationChanged() { + ThreadUtils.assertOnUiThread(); + GeckoScreenOrientation.getInstance().update(); + } + + /** + * Notify Gecko that the device configuration has changed. + * + * @param newConfig The new Configuration object, {@link android.content.res.Configuration}. + */ + @UiThread + public void configurationChanged(final @NonNull Configuration newConfig) { + ThreadUtils.assertOnUiThread(); + GeckoSystemStateListener.getInstance().updateNightMode(newConfig.uiMode); + } + + /** + * Notify Gecko that the screen orientation has changed. + * + * @param newOrientation The new screen orientation, as retrieved e.g. from the current {@link + * android.content.res.Configuration}. + */ + @UiThread + public void orientationChanged(final int newOrientation) { + ThreadUtils.assertOnUiThread(); + GeckoScreenOrientation.getInstance().update(newOrientation); + } + + /** + * Get the orientation controller for this runtime. The orientation controller can be used to + * manage changes to and locking of the screen orientation. + * + * @return The {@link OrientationController} for this instance. + */ + @UiThread + public @NonNull OrientationController getOrientationController() { + ThreadUtils.assertOnUiThread(); + + if (mOrientationController == null) { + mOrientationController = new OrientationController(); + } + return mOrientationController; + } + + /** + * Converts GeckoScreenOrientation to ActivityInfo orientation + * + * @return A {@link ActivityInfo} orientation. + */ + @AnyThread + private int toAndroidOrientation(final int geckoOrientation) { + if (geckoOrientation == ScreenOrientation.PORTRAIT_PRIMARY.value) { + return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } else if (geckoOrientation == ScreenOrientation.PORTRAIT_SECONDARY.value) { + return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } else if (geckoOrientation == ScreenOrientation.LANDSCAPE_PRIMARY.value) { + return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + } else if (geckoOrientation == ScreenOrientation.LANDSCAPE_SECONDARY.value) { + return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } else if (geckoOrientation == ScreenOrientation.DEFAULT.value) { + return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + } else if (geckoOrientation == ScreenOrientation.PORTRAIT.value) { + return ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } else if (geckoOrientation == ScreenOrientation.LANDSCAPE.value) { + return ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else if (geckoOrientation == ScreenOrientation.ANY.value) { + return ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; + } + return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } + + /** + * Lock screen orientation using OrientationController's onOrientationLock. + * + * @return A {@link GeckoResult} that resolves an orientation lock. + */ + @WrapForJNI(calledFrom = "gecko") + private @NonNull GeckoResult<Boolean> lockScreenOrientation(final int aOrientation) { + final GeckoResult<Boolean> res = new GeckoResult<>(); + ThreadUtils.runOnUiThread( + () -> { + final OrientationController.OrientationDelegate delegate = + getOrientationController().getDelegate(); + if (delegate == null) { + // Delegate is not set + res.completeExceptionally(new Exception("Not supported")); + return; + } + final GeckoResult<AllowOrDeny> response = + delegate.onOrientationLock(toAndroidOrientation(aOrientation)); + if (response == null) { + // Delegate is default. So lock orientation is not implemented + res.completeExceptionally(new Exception("Not supported")); + return; + } + res.completeFrom(response.map(v -> v == AllowOrDeny.ALLOW)); + }); + return res; + } + + /** Unlock screen orientation using OrientationController's onOrientationUnlock. */ + @WrapForJNI(calledFrom = "gecko") + private void unlockScreenOrientation() { + ThreadUtils.runOnUiThread( + () -> { + final OrientationController.OrientationDelegate delegate = + getOrientationController().getDelegate(); + if (delegate != null) { + delegate.onOrientationUnlock(); + } + }); + } + + /** + * Get the storage controller for this runtime. The storage controller can be used to manage + * persistent storage data accumulated by {@link GeckoSession}. + * + * @return The {@link StorageController} for this instance. + */ + @UiThread + public @NonNull StorageController getStorageController() { + ThreadUtils.assertOnUiThread(); + + if (mStorageController == null) { + mStorageController = new StorageController(); + } + return mStorageController; + } + + /** + * Get the Web Push controller for this runtime. The Web Push controller can be used to allow + * content to use the Web Push API. + * + * @return The {@link WebPushController} for this instance. + */ + @UiThread + public @NonNull WebPushController getWebPushController() { + ThreadUtils.assertOnUiThread(); + + if (mPushController == null) { + mPushController = new WebPushController(); + } + + return mPushController; + } + + /** + * Appends notes to crash report. + * + * @param notes The application notes to append to the crash report. + */ + @AnyThread + public void appendAppNotesToCrashReport(@NonNull final String notes) { + final String notesWithNewLine = notes + "\n"; + if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { + GeckoAppShell.nativeAppendAppNotesToCrashReport(notesWithNewLine); + } else { + GeckoThread.queueNativeCallUntil( + GeckoThread.State.PROFILE_READY, + GeckoAppShell.class, + "nativeAppendAppNotesToCrashReport", + String.class, + notesWithNewLine); + } + // This function already adds a newline + GeckoAppShell.appendAppNotesToCrashReport(notes); + } + + @Override // Parcelable + @AnyThread + public int describeContents() { + return 0; + } + + @Override // Parcelable + @AnyThread + public void writeToParcel(final Parcel out, final int flags) { + out.writeParcelable(mSettings, flags); + } + + // AIDL code may call readFromParcel even though it's not part of Parcelable. + @AnyThread + @SuppressWarnings("checkstyle:javadocmethod") + public void readFromParcel(final @NonNull Parcel source) { + mSettings = source.readParcelable(getClass().getClassLoader()); + } + + public static final Parcelable.Creator<GeckoRuntime> CREATOR = + new Parcelable.Creator<GeckoRuntime>() { + @Override + @AnyThread + public GeckoRuntime createFromParcel(final Parcel in) { + final GeckoRuntime runtime = new GeckoRuntime(); + runtime.readFromParcel(in); + return runtime; + } + + @Override + @AnyThread + public GeckoRuntime[] newArray(final int size) { + return new GeckoRuntime[size]; + } + }; +} |