/* -*- 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 Sentry and Mozilla's Socorro.
*
* 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 * *
Key=Value
* * 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"; /** * This is a key for extra data sent with {@link #ACTION_CRASHED}. The value is a String * containing the content process type, which might not be available even for child processes. * * @see GeckoSession.ContentDelegate#onCrash(GeckoSession) */ public static final String EXTRA_CRASH_REMOTE_TYPE = "remoteType"; /** * 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(); } else { // Notify Gecko when the application has been moved in the foreground for the first time // after being created and started (used by the ExtensionProcessCrashObserver on the Gecko // side to adjust the appIsForeground property when the application-foreground or // application-background topics are not notified). EventDispatcher.getInstance().dispatch("GeckoView:InitialForeground", null); } 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. * *

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(); mScreenChangeListener = new GeckoScreenChangeListener(); 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 serviceWorkerOpenWindow(final @NonNull String url) { if (sRuntime != null && sRuntime.mServiceWorkerDelegate != null) { final GeckoResult 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) value -> { callback.sendSuccess(value); return null; }) .exceptionally( (GeckoResult.OnExceptionListener) 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)); i.putExtra(EXTRA_CRASH_REMOTE_TYPE, message.getString(EXTRA_CRASH_REMOTE_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 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 prefs = settings.getPrefsMap(); // Older versions have problems with SnakeYaml 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 = Settings.Global.getString(context.getContentResolver(), Settings.Global.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. * *

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. * *

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 Service * Worker API * @see openWindow() */ @UiThread @NonNull GeckoResult onOpenWindow(@NonNull String url); } /** * Sets the {@link ServiceWorkerDelegate} to be used for Service Worker requests. * * @param serviceWorkerDelegate An instance of {@link ServiceWorkerDelegate}. * @see Service * Worker API */ @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 Web * Notifications */ @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 Web * Authentication API */ 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 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 startActivityForResult(final @NonNull PendingIntent intent) { if (!ThreadUtils.isOnUiThread()) { // Delegates expect to be called on the UI thread. final GeckoResult result = new GeckoResult<>(); ThreadUtils.runOnUiThread( () -> { final GeckoResult 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 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 lockScreenOrientation(final int aOrientation) { final GeckoResult 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 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 CREATOR = new Parcelable.Creator() { @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]; } }; }