summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview_example
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /mobile/android/geckoview_example
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/geckoview_example')
-rw-r--r--mobile/android/geckoview_example/build.gradle59
-rw-r--r--mobile/android/geckoview_example/proguard-rules.pro17
-rw-r--r--mobile/android/geckoview_example/src/main/AndroidManifest.xml68
-rw-r--r--mobile/android/geckoview_example/src/main/assets/error.html28
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java18
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java1016
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ExampleCrashHandler.java123
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java2209
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewBottomBehavior.java27
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java64
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/NestedGeckoView.java174
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SessionActivity.java4
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SettingsActivity.java40
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java43
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java119
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarBottomBehavior.java64
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java126
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_camera.pngbin0 -> 267 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic.pngbin0 -> 474 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic_camera.pngbin0 -> 242 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_crash.pngbin0 -> 303 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_status_logo.pngbin0 -> 717 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-mdpi/ic_crash.pngbin0 -> 226 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_camera.pngbin0 -> 319 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic.pngbin0 -> 575 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic_camera.pngbin0 -> 268 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xhdpi/ic_crash.pngbin0 -> 386 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_camera.pngbin0 -> 428 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic.pngbin0 -> 829 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic_camera.pngbin0 -> 487 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/ic_crash.pngbin0 -> 572 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/logo.pngbin0 -> 3808 bytes
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml9
-rw-r--r--mobile/android/geckoview_example/src/main/res/drawable/tab_number_background.xml20
-rw-r--r--mobile/android/geckoview_example/src/main/res/layout/activity_settings.xml24
-rw-r--r--mobile/android/geckoview_example/src/main/res/layout/browser_action.xml32
-rw-r--r--mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml13
-rw-r--r--mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml32
-rw-r--r--mobile/android/geckoview_example/src/main/res/menu/actions.xml15
-rw-r--r--mobile/android/geckoview_example/src/main/res/values/colors.xml7
-rw-r--r--mobile/android/geckoview_example/src/main/res/values/ids.xml7
-rw-r--r--mobile/android/geckoview_example/src/main/res/values/strings.xml133
-rw-r--r--mobile/android/geckoview_example/src/main/res/values/styles.xml7
-rw-r--r--mobile/android/geckoview_example/src/main/res/xml/settings.xml57
44 files changed, 4555 insertions, 0 deletions
diff --git a/mobile/android/geckoview_example/build.gradle b/mobile/android/geckoview_example/build.gradle
new file mode 100644
index 0000000000..dac3e2dc03
--- /dev/null
+++ b/mobile/android/geckoview_example/build.gradle
@@ -0,0 +1,59 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview_example"
+
+apply plugin: 'com.android.application'
+
+apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
+
+android {
+ buildToolsVersion project.ext.buildToolsVersion
+ compileSdkVersion project.ext.compileSdkVersion
+
+ defaultConfig {
+ targetSdkVersion project.ext.targetSdkVersion
+ minSdkVersion project.ext.minSdkVersion
+ manifestPlaceholders = project.ext.manifestPlaceholders
+
+ applicationId "org.mozilla.geckoview_example"
+ versionCode 1
+ versionName "1.0"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ dexOptions {
+ javaMaxHeapSize "6g"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ // By default the android plugins ignores folders that start with `_`, but
+ // we need those in web extensions.
+ // See also:
+ // - https://issuetracker.google.com/issues/36911326
+ // - https://stackoverflow.com/questions/9206117/how-to-workaround-autoomitting-fiiles-folders-starting-with-underscore-in
+ aaptOptions {
+ ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
+ noCompress 'ja'
+ }
+
+ project.configureProductFlavors.delegate = it
+ project.configureProductFlavors()
+}
+
+dependencies {
+ implementation "androidx.annotation:annotation:1.0.0"
+ implementation "androidx.appcompat:appcompat:1.0.0"
+ implementation "androidx.preference:preference:1.0.0"
+
+ implementation project(path: ':geckoview')
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'com.google.android.material:material:1.0.0'
+}
diff --git a/mobile/android/geckoview_example/proguard-rules.pro b/mobile/android/geckoview_example/proguard-rules.pro
new file mode 100644
index 0000000000..46fbee5497
--- /dev/null
+++ b/mobile/android/geckoview_example/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/nalexander/.mozbuild/android-sdk-macosx/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/mobile/android/geckoview_example/src/main/AndroidManifest.xml b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..7ee8c4cf6e
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.geckoview_example">
+
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.CAMERA"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+
+ <application
+ android:allowBackup="true"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:usesCleartextTraffic="true"
+ android:icon="@drawable/logo">
+ <uses-library android:name="android.test.runner"
+ android:required="false"/>
+
+ <activity
+ android:name=".GeckoViewActivity"
+ android:label="GeckoView Example"
+ android:launchMode="singleTop"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar"
+ android:windowSoftInputMode="stateUnspecified|adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
+ <category android:name="android.intent.category.APP_BROWSER"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="http"/>
+ <data android:scheme="https"/>
+ <data android:scheme="about"/>
+ <data android:scheme="javascript"/>
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".SessionActivity"
+ android:label="GeckoView Example"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar"
+ android:windowSoftInputMode="stateUnspecified|adjustResize">
+ </activity>
+ <activity
+ android:name=".SettingsActivity"
+ android:label="Settings"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar">
+ </activity>
+
+ <service
+ android:name=".ExampleCrashHandler"
+ android:exported="false"
+ android:process=":crash">
+ </service>
+ </application>
+
+</manifest>
diff --git a/mobile/android/geckoview_example/src/main/assets/error.html b/mobile/android/geckoview_example/src/main/assets/error.html
new file mode 100644
index 0000000000..9d2c426df3
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/assets/error.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Boom!</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
+ <style>
+ body {
+ background-color: red;
+ color: white;
+ font-family: sans;
+ }
+
+ div.container {
+ width: 75%;
+ margin: auto;
+ text-align: center;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>Boom!</h1>
+ <p>Something bad happened...</p>
+ <p>$ERROR</p>
+ </div>
+</body>
+</html>
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java
new file mode 100644
index 0000000000..7e3c036061
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java
@@ -0,0 +1,18 @@
+package org.mozilla.geckoview_example;
+
+import android.graphics.Bitmap;
+
+public class ActionButton {
+ final Bitmap icon;
+ final String text;
+ final Integer textColor;
+ final Integer backgroundColor;
+
+ public ActionButton(final Bitmap icon, final String text, final Integer textColor,
+ final Integer backgroundColor) {
+ this.icon = icon;
+ this.text = text;
+ this.textColor = textColor;
+ this.backgroundColor = backgroundColor;
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
new file mode 100644
index 0000000000..686c6a0f71
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
@@ -0,0 +1,1016 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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_example;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.text.InputType;
+import android.text.format.DateFormat;
+import android.util.Log;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.CheckedTextView;
+import android.widget.DatePicker;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.ScrollView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.TimePicker;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+import org.mozilla.geckoview.AllowOrDeny;
+import org.mozilla.geckoview.GeckoResult;
+import org.mozilla.geckoview.GeckoSession;
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource;
+import org.mozilla.geckoview.SlowScriptResponse;
+
+final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
+ protected static final String LOGTAG = "BasicGeckoViewPrompt";
+
+ private final Activity mActivity;
+ public int filePickerRequestCode = 1;
+ private int mFileType;
+ private GeckoResult<PromptResponse> mFileResponse;
+ private FilePrompt mFilePrompt;
+
+ public BasicGeckoViewPrompt(final Activity activity) {
+ mActivity = activity;
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onAlertPrompt(final GeckoSession session,
+ final AlertPrompt prompt) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
+ .setTitle(prompt.title)
+ .setMessage(prompt.message)
+ .setPositiveButton(android.R.string.ok, /* onClickListener */ null);
+ GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
+ createStandardDialog(builder, prompt, res).show();
+ return res;
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onButtonPrompt(final GeckoSession session,
+ final ButtonPrompt prompt) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
+ .setTitle(prompt.title)
+ .setMessage(prompt.message);
+
+ GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
+
+ final DialogInterface.OnClickListener listener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ res.complete(prompt.confirm(ButtonPrompt.Type.POSITIVE));
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ res.complete(prompt.confirm(ButtonPrompt.Type.NEGATIVE));
+ } else {
+ res.complete(prompt.dismiss());
+ }
+ }
+ };
+
+ builder.setPositiveButton(android.R.string.ok, listener);
+ builder.setNegativeButton(android.R.string.cancel, listener);
+
+ createStandardDialog(builder, prompt, res).show();
+ return res;
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onSharePrompt(final GeckoSession session,
+ final SharePrompt prompt) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+
+ @Nullable
+ @Override
+ public GeckoResult<PromptResponse> onRepostConfirmPrompt(final GeckoSession session,
+ final RepostConfirmPrompt prompt) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
+ .setTitle(R.string.repost_confirm_title)
+ .setMessage(R.string.repost_confirm_message);
+
+ GeckoResult<PromptResponse> res = new GeckoResult<>();
+
+ final DialogInterface.OnClickListener listener = (dialog, which) -> {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ res.complete(prompt.confirm(AllowOrDeny.ALLOW));
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ res.complete(prompt.confirm(AllowOrDeny.DENY));
+ } else {
+ res.complete(prompt.dismiss());
+ }
+ };
+
+ builder.setPositiveButton(R.string.repost_confirm_resend, listener);
+ builder.setNegativeButton(R.string.repost_confirm_cancel, listener);
+
+ createStandardDialog(builder, prompt, res).show();
+ return res;
+ }
+
+ @Nullable
+ @Override
+ public GeckoResult<PromptResponse> onBeforeUnloadPrompt(final GeckoSession session,
+ final BeforeUnloadPrompt prompt) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
+ .setTitle(R.string.before_unload_title)
+ .setMessage(R.string.before_unload_message);
+
+ GeckoResult<PromptResponse> res = new GeckoResult<>();
+
+ final DialogInterface.OnClickListener listener = (dialog, which) -> {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ res.complete(prompt.confirm(AllowOrDeny.ALLOW));
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ res.complete(prompt.confirm(AllowOrDeny.DENY));
+ } else {
+ res.complete(prompt.dismiss());
+ }
+ };
+
+ builder.setPositiveButton(R.string.before_unload_leave_page, listener);
+ builder.setNegativeButton(R.string.before_unload_stay, listener);
+
+ createStandardDialog(builder, prompt, res).show();
+ return res;
+ }
+
+ private int getViewPadding(final AlertDialog.Builder builder) {
+ final TypedArray attr = builder.getContext().obtainStyledAttributes(
+ new int[] { android.R.attr.listPreferredItemPaddingLeft });
+ final int padding = attr.getDimensionPixelSize(0, 1);
+ attr.recycle();
+ return padding;
+ }
+
+ private LinearLayout addStandardLayout(final AlertDialog.Builder builder,
+ final String title, final String msg) {
+ final ScrollView scrollView = new ScrollView(builder.getContext());
+ final LinearLayout container = new LinearLayout(builder.getContext());
+ final int horizontalPadding = getViewPadding(builder);
+ final int verticalPadding = (msg == null || msg.isEmpty()) ? horizontalPadding : 0;
+ container.setOrientation(LinearLayout.VERTICAL);
+ container.setPadding(/* left */ horizontalPadding, /* top */ verticalPadding,
+ /* right */ horizontalPadding, /* bottom */ verticalPadding);
+ scrollView.addView(container);
+ builder.setTitle(title)
+ .setMessage(msg)
+ .setView(scrollView);
+ return container;
+ }
+
+ private AlertDialog createStandardDialog(final AlertDialog.Builder builder,
+ final BasePrompt prompt,
+ final GeckoResult<PromptResponse> response) {
+ final AlertDialog dialog = builder.create();
+ dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(final DialogInterface dialog) {
+ if (!prompt.isComplete()) {
+ response.complete(prompt.dismiss());
+ }
+ }
+ });
+ return dialog;
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onTextPrompt(final GeckoSession session,
+ final TextPrompt prompt) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ final LinearLayout container = addStandardLayout(builder, prompt.title, prompt.message);
+ final EditText editText = new EditText(builder.getContext());
+ editText.setText(prompt.defaultValue);
+ container.addView(editText);
+
+ GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
+
+ builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ res.complete(prompt.confirm(editText.getText().toString()));
+ }
+ });
+
+ createStandardDialog(builder, prompt, res).show();
+ return res;
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onAuthPrompt(final GeckoSession session,
+ final AuthPrompt prompt) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ final LinearLayout container = addStandardLayout(builder, prompt.title, prompt.message);
+
+ final int flags = prompt.authOptions.flags;
+ final int level = prompt.authOptions.level;
+ final EditText username;
+ if ((flags & AuthPrompt.AuthOptions.Flags.ONLY_PASSWORD) == 0) {
+ username = new EditText(builder.getContext());
+ username.setHint(R.string.username);
+ username.setText(prompt.authOptions.username);
+ container.addView(username);
+ } else {
+ username = null;
+ }
+
+ final EditText password = new EditText(builder.getContext());
+ password.setHint(R.string.password);
+ password.setText(prompt.authOptions.password);
+ password.setInputType(InputType.TYPE_CLASS_TEXT |
+ InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ container.addView(password);
+
+ if (level != AuthPrompt.AuthOptions.Level.NONE) {
+ final ImageView secure = new ImageView(builder.getContext());
+ secure.setImageResource(android.R.drawable.ic_lock_lock);
+ container.addView(secure);
+ }
+
+ GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
+
+ builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ if ((flags & AuthPrompt.AuthOptions.Flags.ONLY_PASSWORD) == 0) {
+ res.complete(prompt.confirm(username.getText().toString(),
+ password.getText().toString()));
+
+ } else {
+ res.complete(prompt.confirm(password.getText().toString()));
+ }
+ }
+ });
+ createStandardDialog(builder, prompt, res).show();
+
+ return res;
+ }
+
+ private static class ModifiableChoice {
+ public boolean modifiableSelected;
+ public String modifiableLabel;
+ public final ChoicePrompt.Choice choice;
+
+ public ModifiableChoice(ChoicePrompt.Choice c) {
+ choice = c;
+ modifiableSelected = choice.selected;
+ modifiableLabel = choice.label;
+ }
+ }
+
+ private void addChoiceItems(final int type, final ArrayAdapter<ModifiableChoice> list,
+ final ChoicePrompt.Choice[] items, final String indent) {
+ if (type == ChoicePrompt.Type.MENU) {
+ for (final ChoicePrompt.Choice item : items) {
+ list.add(new ModifiableChoice(item));
+ }
+ return;
+ }
+
+ for (final ChoicePrompt.Choice item : items) {
+ final ModifiableChoice modItem = new ModifiableChoice(item);
+
+ final ChoicePrompt.Choice[] children = item.items;
+
+ if (indent != null && children == null) {
+ modItem.modifiableLabel = indent + modItem.modifiableLabel;
+ }
+ list.add(modItem);
+
+ if (children != null) {
+ final String newIndent;
+ if (type == ChoicePrompt.Type.SINGLE || type == ChoicePrompt.Type.MULTIPLE) {
+ newIndent = (indent != null) ? indent + '\t' : "\t";
+ } else {
+ newIndent = null;
+ }
+ addChoiceItems(type, list, children, newIndent);
+ }
+ }
+ }
+
+ private void onChoicePromptImpl(final GeckoSession session, final String title,
+ final String message, final int type,
+ final ChoicePrompt.Choice[] choices, final ChoicePrompt prompt,
+ final GeckoResult<PromptResponse> res) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ res.complete(prompt.dismiss());
+ return;
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ addStandardLayout(builder, title, message);
+
+ final ListView list = new ListView(builder.getContext());
+ if (type == ChoicePrompt.Type.MULTIPLE) {
+ list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ }
+
+ final ArrayAdapter<ModifiableChoice> adapter = new ArrayAdapter<ModifiableChoice>(
+ builder.getContext(), android.R.layout.simple_list_item_1) {
+ private static final int TYPE_MENU_ITEM = 0;
+ private static final int TYPE_MENU_CHECK = 1;
+ private static final int TYPE_SEPARATOR = 2;
+ private static final int TYPE_GROUP = 3;
+ private static final int TYPE_SINGLE = 4;
+ private static final int TYPE_MULTIPLE = 5;
+ private static final int TYPE_COUNT = 6;
+
+ private LayoutInflater mInflater;
+ private View mSeparator;
+
+ @Override
+ public int getViewTypeCount() {
+ return TYPE_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(final int position) {
+ final ModifiableChoice item = getItem(position);
+ if (item.choice.separator) {
+ return TYPE_SEPARATOR;
+ } else if (type == ChoicePrompt.Type.MENU) {
+ return item.modifiableSelected ? TYPE_MENU_CHECK : TYPE_MENU_ITEM;
+ } else if (item.choice.items != null) {
+ return TYPE_GROUP;
+ } else if (type == ChoicePrompt.Type.SINGLE) {
+ return TYPE_SINGLE;
+ } else if (type == ChoicePrompt.Type.MULTIPLE) {
+ return TYPE_MULTIPLE;
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public boolean isEnabled(final int position) {
+ final ModifiableChoice item = getItem(position);
+ return !item.choice.separator && !item.choice.disabled &&
+ ((type != ChoicePrompt.Type.SINGLE &&
+ type != ChoicePrompt.Type.MULTIPLE) ||
+ item.choice.items == null);
+ }
+
+ @Override
+ public View getView(final int position, View view,
+ final ViewGroup parent) {
+ final int itemType = getItemViewType(position);
+ final int layoutId;
+ if (itemType == TYPE_SEPARATOR) {
+ if (mSeparator == null) {
+ mSeparator = new View(getContext());
+ mSeparator.setLayoutParams(new ListView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, 2, itemType));
+ final TypedArray attr = getContext().obtainStyledAttributes(
+ new int[] { android.R.attr.listDivider });
+ mSeparator.setBackgroundResource(attr.getResourceId(0, 0));
+ attr.recycle();
+ }
+ return mSeparator;
+ } else if (itemType == TYPE_MENU_ITEM) {
+ layoutId = android.R.layout.simple_list_item_1;
+ } else if (itemType == TYPE_MENU_CHECK) {
+ layoutId = android.R.layout.simple_list_item_checked;
+ } else if (itemType == TYPE_GROUP) {
+ layoutId = android.R.layout.preference_category;
+ } else if (itemType == TYPE_SINGLE) {
+ layoutId = android.R.layout.simple_list_item_single_choice;
+ } else if (itemType == TYPE_MULTIPLE) {
+ layoutId = android.R.layout.simple_list_item_multiple_choice;
+ } else {
+ throw new UnsupportedOperationException();
+ }
+
+ if (view == null) {
+ if (mInflater == null) {
+ mInflater = LayoutInflater.from(builder.getContext());
+ }
+ view = mInflater.inflate(layoutId, parent, false);
+ }
+
+ final ModifiableChoice item = getItem(position);
+ final TextView text = (TextView) view;
+ text.setEnabled(!item.choice.disabled);
+ text.setText(item.modifiableLabel);
+ if (view instanceof CheckedTextView) {
+ final boolean selected = item.modifiableSelected;
+ if (itemType == TYPE_MULTIPLE) {
+ list.setItemChecked(position, selected);
+ } else {
+ ((CheckedTextView) view).setChecked(selected);
+ }
+ }
+ return view;
+ }
+ };
+ addChoiceItems(type, adapter, choices, /* indent */ null);
+
+ list.setAdapter(adapter);
+ builder.setView(list);
+
+ final AlertDialog dialog;
+ if (type == ChoicePrompt.Type.SINGLE || type == ChoicePrompt.Type.MENU) {
+ dialog = createStandardDialog(builder, prompt, res);
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View v,
+ final int position, final long id) {
+ final ModifiableChoice item = adapter.getItem(position);
+ if (type == ChoicePrompt.Type.MENU) {
+ final ChoicePrompt.Choice[] children = item.choice.items;
+ if (children != null) {
+ // Show sub-menu.
+ dialog.setOnDismissListener(null);
+ dialog.dismiss();
+ onChoicePromptImpl(session, item.modifiableLabel, /* msg */ null,
+ type, children, prompt, res);
+ return;
+ }
+ }
+ res.complete(prompt.confirm(item.choice));
+ dialog.dismiss();
+ }
+ });
+ } else if (type == ChoicePrompt.Type.MULTIPLE) {
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View v,
+ final int position, final long id) {
+ final ModifiableChoice item = adapter.getItem(position);
+ item.modifiableSelected = ((CheckedTextView) v).isChecked();
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog,
+ final int which) {
+ final int len = adapter.getCount();
+ ArrayList<String> items = new ArrayList<>(len);
+ for (int i = 0; i < len; i++) {
+ final ModifiableChoice item = adapter.getItem(i);
+ if (item.modifiableSelected) {
+ items.add(item.choice.id);
+ }
+ }
+ res.complete(prompt.confirm(items.toArray(new String[items.size()])));
+ }
+ });
+ dialog = createStandardDialog(builder, prompt, res);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ dialog.show();
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onChoicePrompt(final GeckoSession session,
+ final ChoicePrompt prompt) {
+ final GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
+ onChoicePromptImpl(session, prompt.title, prompt.message, prompt.type, prompt.choices,
+ prompt, res);
+ return res;
+ }
+
+ private static int parseColor(final String value, final int def) {
+ try {
+ return Color.parseColor(value);
+ } catch (final IllegalArgumentException e) {
+ return def;
+ }
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onColorPrompt(final GeckoSession session,
+ final ColorPrompt prompt) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ addStandardLayout(builder, prompt.title, /* msg */ null);
+
+ final int initial = parseColor(prompt.defaultValue, /* def */ 0);
+ final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(
+ builder.getContext(), android.R.layout.simple_list_item_1) {
+ private LayoutInflater mInflater;
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public int getItemViewType(final int position) {
+ return (getItem(position) == initial) ? 1 : 0;
+ }
+
+ @Override
+ public View getView(final int position, View view,
+ final ViewGroup parent) {
+ if (mInflater == null) {
+ mInflater = LayoutInflater.from(builder.getContext());
+ }
+ final int color = getItem(position);
+ if (view == null) {
+ view = mInflater.inflate((color == initial) ?
+ android.R.layout.simple_list_item_checked :
+ android.R.layout.simple_list_item_1, parent, false);
+ }
+ view.setBackgroundResource(android.R.drawable.editbox_background);
+ view.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
+ return view;
+ }
+ };
+
+ adapter.addAll(0xffff4444 /* holo_red_light */,
+ 0xffcc0000 /* holo_red_dark */,
+ 0xffffbb33 /* holo_orange_light */,
+ 0xffff8800 /* holo_orange_dark */,
+ 0xff99cc00 /* holo_green_light */,
+ 0xff669900 /* holo_green_dark */,
+ 0xff33b5e5 /* holo_blue_light */,
+ 0xff0099cc /* holo_blue_dark */,
+ 0xffaa66cc /* holo_purple */,
+ 0xffffffff /* white */,
+ 0xffaaaaaa /* lighter_gray */,
+ 0xff555555 /* darker_gray */,
+ 0xff000000 /* black */);
+
+ final ListView list = new ListView(builder.getContext());
+ list.setAdapter(adapter);
+ builder.setView(list);
+
+ GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
+
+ final AlertDialog dialog = createStandardDialog(builder, prompt, res);
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View v,
+ final int position, final long id) {
+ res.complete(prompt.confirm(String.format("#%06x", 0xffffff & adapter.getItem(position))));
+ dialog.dismiss();
+ }
+ });
+ dialog.show();
+
+ return res;
+ }
+
+ private static Date parseDate(final SimpleDateFormat formatter,
+ final String value,
+ final boolean defaultToNow) {
+ try {
+ if (value != null && !value.isEmpty()) {
+ return formatter.parse(value);
+ }
+ } catch (final ParseException e) {
+ }
+ return defaultToNow ? new Date() : null;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static void setTimePickerTime(final TimePicker picker, final Calendar cal) {
+ if (Build.VERSION.SDK_INT >= 23) {
+ picker.setHour(cal.get(Calendar.HOUR_OF_DAY));
+ picker.setMinute(cal.get(Calendar.MINUTE));
+ } else {
+ picker.setCurrentHour(cal.get(Calendar.HOUR_OF_DAY));
+ picker.setCurrentMinute(cal.get(Calendar.MINUTE));
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private static void setCalendarTime(final Calendar cal, final TimePicker picker) {
+ if (Build.VERSION.SDK_INT >= 23) {
+ cal.set(Calendar.HOUR_OF_DAY, picker.getHour());
+ cal.set(Calendar.MINUTE, picker.getMinute());
+ } else {
+ cal.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour());
+ cal.set(Calendar.MINUTE, picker.getCurrentMinute());
+ }
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onDateTimePrompt(final GeckoSession session,
+ final DateTimePrompt prompt) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+ final String format;
+ if (prompt.type == DateTimePrompt.Type.DATE) {
+ format = "yyyy-MM-dd";
+ } else if (prompt.type == DateTimePrompt.Type.MONTH) {
+ format = "yyyy-MM";
+ } else if (prompt.type == DateTimePrompt.Type.WEEK) {
+ format = "yyyy-'W'ww";
+ } else if (prompt.type == DateTimePrompt.Type.TIME) {
+ format = "HH:mm";
+ } else if (prompt.type == DateTimePrompt.Type.DATETIME_LOCAL) {
+ format = "yyyy-MM-dd'T'HH:mm";
+ } else {
+ throw new UnsupportedOperationException();
+ }
+
+ final SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.ROOT);
+ final Date minDate = parseDate(formatter, prompt.minValue, /* defaultToNow */ false);
+ final Date maxDate = parseDate(formatter, prompt.maxValue, /* defaultToNow */ false);
+ final Date date = parseDate(formatter, prompt.defaultValue, /* defaultToNow */ true);
+ final Calendar cal = formatter.getCalendar();
+ cal.setTime(date);
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ final LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+ final DatePicker datePicker;
+ if (prompt.type == DateTimePrompt.Type.DATE || prompt.type == DateTimePrompt.Type.MONTH ||
+ prompt.type == DateTimePrompt.Type.WEEK || prompt.type == DateTimePrompt.Type.DATETIME_LOCAL) {
+ final int resId = builder.getContext().getResources().getIdentifier(
+ "date_picker_dialog", "layout", "android");
+ DatePicker picker = null;
+ if (resId != 0) {
+ try {
+ picker = (DatePicker) inflater.inflate(resId, /* root */ null);
+ } catch (final ClassCastException|InflateException e) {
+ }
+ }
+ if (picker == null) {
+ picker = new DatePicker(builder.getContext());
+ }
+ picker.init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH),
+ cal.get(Calendar.DAY_OF_MONTH), /* listener */ null);
+ if (minDate != null) {
+ picker.setMinDate(minDate.getTime());
+ }
+ if (maxDate != null) {
+ picker.setMaxDate(maxDate.getTime());
+ }
+ datePicker = picker;
+ } else {
+ datePicker = null;
+ }
+
+ final TimePicker timePicker;
+ if (prompt.type == DateTimePrompt.Type.TIME || prompt.type == DateTimePrompt.Type.DATETIME_LOCAL) {
+ final int resId = builder.getContext().getResources().getIdentifier(
+ "time_picker_dialog", "layout", "android");
+ TimePicker picker = null;
+ if (resId != 0) {
+ try {
+ picker = (TimePicker) inflater.inflate(resId, /* root */ null);
+ } catch (final ClassCastException|InflateException e) {
+ }
+ }
+ if (picker == null) {
+ picker = new TimePicker(builder.getContext());
+ }
+ setTimePickerTime(picker, cal);
+ picker.setIs24HourView(DateFormat.is24HourFormat(builder.getContext()));
+ timePicker = picker;
+ } else {
+ timePicker = null;
+ }
+
+ final LinearLayout container = addStandardLayout(builder, prompt.title, /* msg */ null);
+ container.setPadding(/* left */ 0, /* top */ 0, /* right */ 0, /* bottom */ 0);
+ if (datePicker != null) {
+ container.addView(datePicker);
+ }
+ if (timePicker != null) {
+ container.addView(timePicker);
+ }
+
+ GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
+
+ final DialogInterface.OnClickListener listener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ if (which == DialogInterface.BUTTON_NEUTRAL) {
+ // Clear
+ res.complete(prompt.confirm(""));
+ return;
+ }
+ if (datePicker != null) {
+ cal.set(datePicker.getYear(), datePicker.getMonth(),
+ datePicker.getDayOfMonth());
+ }
+ if (timePicker != null) {
+ setCalendarTime(cal, timePicker);
+ }
+ res.complete(prompt.confirm(formatter.format(cal.getTime())));
+ }
+ };
+ builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+ .setNeutralButton(R.string.clear_field, listener)
+ .setPositiveButton(android.R.string.ok, listener);
+ createStandardDialog(builder, prompt, res).show();
+
+ return res;
+ }
+
+ @Override
+ @TargetApi(19)
+ public GeckoResult<PromptResponse> onFilePrompt(GeckoSession session, FilePrompt prompt)
+ {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+
+ // Merge all given MIME types into one, using wildcard if needed.
+ String mimeType = null;
+ String mimeSubtype = null;
+ if (prompt.mimeTypes != null) {
+ for (final String rawType : prompt.mimeTypes) {
+ final String normalizedType = rawType.trim().toLowerCase(Locale.ROOT);
+ final int len = normalizedType.length();
+ int slash = normalizedType.indexOf('/');
+ if (slash < 0) {
+ slash = len;
+ }
+ final String newType = normalizedType.substring(0, slash);
+ final String newSubtype = normalizedType.substring(Math.min(slash + 1, len));
+ if (mimeType == null) {
+ mimeType = newType;
+ } else if (!mimeType.equals(newType)) {
+ mimeType = "*";
+ }
+ if (mimeSubtype == null) {
+ mimeSubtype = newSubtype;
+ } else if (!mimeSubtype.equals(newSubtype)) {
+ mimeSubtype = "*";
+ }
+ }
+ }
+
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType((mimeType != null ? mimeType : "*") + '/' +
+ (mimeSubtype != null ? mimeSubtype : "*"));
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
+ if (Build.VERSION.SDK_INT >= 18 && prompt.type == FilePrompt.Type.MULTIPLE) {
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+ }
+ if (Build.VERSION.SDK_INT >= 19 && prompt.mimeTypes.length > 0) {
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, prompt.mimeTypes);
+ }
+
+ GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
+
+ try {
+ mFileResponse = res;
+ mFilePrompt = prompt;
+ activity.startActivityForResult(intent, filePickerRequestCode);
+ } catch (final ActivityNotFoundException e) {
+ Log.e(LOGTAG, "Cannot launch activity", e);
+ return GeckoResult.fromValue(prompt.dismiss());
+ }
+
+ return res;
+ }
+
+ public void onFileCallbackResult(final int resultCode, final Intent data) {
+ if (mFileResponse == null) {
+ return;
+ }
+
+ final GeckoResult<PromptResponse> res = mFileResponse;
+ mFileResponse = null;
+
+ final FilePrompt prompt = mFilePrompt;
+ mFilePrompt = null;
+
+ if (resultCode != Activity.RESULT_OK || data == null) {
+ res.complete(prompt.dismiss());
+ return;
+ }
+
+ final Uri uri = data.getData();
+ final ClipData clip = data.getClipData();
+
+ if (prompt.type == FilePrompt.Type.SINGLE ||
+ (prompt.type == FilePrompt.Type.MULTIPLE && clip == null)) {
+ res.complete(prompt.confirm(mActivity, uri));
+ } else if (prompt.type == FilePrompt.Type.MULTIPLE) {
+ if (clip == null) {
+ Log.w(LOGTAG, "No selected file");
+ res.complete(prompt.dismiss());
+ return;
+ }
+ final int count = clip.getItemCount();
+ final ArrayList<Uri> uris = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ uris.add(clip.getItemAt(i).getUri());
+ }
+ res.complete(prompt.confirm(mActivity, uris.toArray(new Uri[uris.size()])));
+ }
+ }
+
+ public void onPermissionPrompt(final GeckoSession session, final String title,
+ final GeckoSession.PermissionDelegate.Callback callback) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ callback.reject();
+ return;
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(title)
+ .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ callback.reject();
+ }
+ })
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ callback.grant();
+ }
+ });
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ public void onSlowScriptPrompt(GeckoSession geckoSession, String title, GeckoResult<SlowScriptResponse> reportAction) {
+ final Activity activity = mActivity;
+ if (activity == null) {
+ return;
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(title)
+ .setNegativeButton(activity.getString(R.string.wait), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ reportAction.complete(SlowScriptResponse.CONTINUE);
+ }
+ })
+ .setPositiveButton(activity.getString(R.string.stop), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ reportAction.complete(SlowScriptResponse.STOP);
+ }
+ });
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private Spinner addMediaSpinner(final Context context, final ViewGroup container,
+ final MediaSource[] sources, final String[] sourceNames) {
+ final ArrayAdapter<MediaSource> adapter = new ArrayAdapter<MediaSource>(
+ context, android.R.layout.simple_spinner_item) {
+ private View convertView(final int position, final View view) {
+ if (view != null) {
+ final MediaSource item = getItem(position);
+ ((TextView) view).setText(sourceNames != null ? sourceNames[position] : item.name);
+ }
+ return view;
+ }
+
+ @Override
+ public View getView(final int position, View view,
+ final ViewGroup parent) {
+ return convertView(position, super.getView(position, view, parent));
+ }
+
+ @Override
+ public View getDropDownView(final int position, final View view,
+ final ViewGroup parent) {
+ return convertView(position, super.getDropDownView(position, view, parent));
+ }
+ };
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ adapter.addAll(sources);
+
+ final Spinner spinner = new Spinner(context);
+ spinner.setAdapter(adapter);
+ spinner.setSelection(0);
+ container.addView(spinner);
+ return spinner;
+ }
+
+ public void onMediaPrompt(final GeckoSession session, final String title,
+ final MediaSource[] video, final MediaSource[] audio,
+ final String[] videoNames, final String[] audioNames,
+ final GeckoSession.PermissionDelegate.MediaCallback callback) {
+ final Activity activity = mActivity;
+ if (activity == null || (video == null && audio == null)) {
+ callback.reject();
+ return;
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ final LinearLayout container = addStandardLayout(builder, title, /* msg */ null);
+
+ final Spinner videoSpinner;
+ if (video != null) {
+ videoSpinner = addMediaSpinner(builder.getContext(), container, video, videoNames);
+ } else {
+ videoSpinner = null;
+ }
+
+ final Spinner audioSpinner;
+ if (audio != null) {
+ audioSpinner = addMediaSpinner(builder.getContext(), container, audio, audioNames);
+ } else {
+ audioSpinner = null;
+ }
+
+ builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ final MediaSource video = (videoSpinner != null)
+ ? (MediaSource) videoSpinner.getSelectedItem() : null;
+ final MediaSource audio = (audioSpinner != null)
+ ? (MediaSource) audioSpinner.getSelectedItem() : null;
+ callback.grant(video, audio);
+ }
+ });
+
+ final AlertDialog dialog = builder.create();
+ dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(final DialogInterface dialog) {
+ callback.reject();
+ }
+ });
+ dialog.show();
+ }
+
+ public void onMediaPrompt(final GeckoSession session, final String title,
+ final MediaSource[] video, final MediaSource[] audio,
+ final GeckoSession.PermissionDelegate.MediaCallback callback) {
+ onMediaPrompt(session, title, video, audio, null, null, callback);
+ }
+
+ @Override
+ public GeckoResult<PromptResponse> onPopupPrompt(final GeckoSession session,
+ final PopupPrompt prompt) {
+ return GeckoResult.fromValue(prompt.confirm(AllowOrDeny.ALLOW));
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ExampleCrashHandler.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ExampleCrashHandler.java
new file mode 100644
index 0000000000..ae462cc6df
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ExampleCrashHandler.java
@@ -0,0 +1,123 @@
+package org.mozilla.geckoview_example;
+
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.geckoview.CrashReporter;
+import org.mozilla.geckoview.GeckoRuntime;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.StrictMode;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import android.util.Log;
+
+public class ExampleCrashHandler extends Service {
+ private static final String LOGTAG = "ExampleCrashHandler";
+
+ private static final String CHANNEL_ID = "geckoview_example_crashes";
+ private static final int NOTIFY_ID = 42;
+
+ private static final String ACTION_REPORT_CRASH = "org.mozilla.geckoview_example.ACTION_REPORT_CRASH";
+ private static final String ACTION_DISMISS = "org.mozilla.geckoview_example.ACTION_DISMISS";
+
+ private Intent mCrashIntent;
+
+ public ExampleCrashHandler() {
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ stopSelf();
+ return Service.START_NOT_STICKY;
+ }
+
+ if (GeckoRuntime.ACTION_CRASHED.equals(intent.getAction())) {
+ mCrashIntent = intent;
+
+ Log.d(LOGTAG, "Dump File: " +
+ mCrashIntent.getStringExtra(GeckoRuntime.EXTRA_MINIDUMP_PATH));
+ Log.d(LOGTAG, "Extras File: " +
+ mCrashIntent.getStringExtra(GeckoRuntime.EXTRA_EXTRAS_PATH));
+ Log.d(LOGTAG, "Fatal: " +
+ mCrashIntent.getBooleanExtra(GeckoRuntime.EXTRA_CRASH_FATAL, false));
+
+ String id = createNotificationChannel();
+
+ PendingIntent reportIntent = PendingIntent.getService(
+ this, 0,
+ new Intent(ACTION_REPORT_CRASH, null,
+ this, ExampleCrashHandler.class), 0);
+
+ PendingIntent dismissIntent = PendingIntent.getService(
+ this, 0,
+ new Intent(ACTION_DISMISS, null,
+ this, ExampleCrashHandler.class), 0);
+
+ Notification notification = new NotificationCompat.Builder(this, id)
+ .setSmallIcon(R.drawable.ic_crash)
+ .setContentTitle(getResources().getString(R.string.crashed_title))
+ .setContentText(getResources().getString(R.string.crashed_text))
+ .setDefaults(Notification.DEFAULT_ALL)
+ .setContentIntent(reportIntent)
+ .addAction(0, getResources().getString(R.string.crashed_ignore), dismissIntent)
+ .setAutoCancel(true)
+ .setOngoing(false)
+ .build();
+
+ startForeground(NOTIFY_ID, notification);
+ } else if (ACTION_REPORT_CRASH.equals(intent.getAction())) {
+ StrictMode.ThreadPolicy oldPolicy = null;
+ if (BuildConfig.DEBUG) {
+ oldPolicy = StrictMode.getThreadPolicy();
+
+ // We do some disk I/O and network I/O on the main thread, but it's fine.
+ StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder(oldPolicy)
+ .permitDiskReads()
+ .permitDiskWrites()
+ .permitNetwork()
+ .build());
+ }
+
+ try {
+ CrashReporter.sendCrashReport(this, mCrashIntent, "GeckoViewExample");
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failed to send crash report", e);
+ }
+
+ if (oldPolicy != null) {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+
+ stopSelf();
+ } else if (ACTION_DISMISS.equals(intent.getAction())) {
+ stopSelf();
+ }
+
+ return Service.START_NOT_STICKY;
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private String createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "GeckoView Example Crashes", NotificationManager.IMPORTANCE_LOW);
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ return CHANNEL_ID;
+ }
+
+ return "";
+ }
+
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
new file mode 100644
index 0000000000..7cf4928766
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -0,0 +1,2209 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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_example;
+
+import org.json.JSONObject;
+
+import org.mozilla.geckoview.AllowOrDeny;
+import org.mozilla.geckoview.BasicSelectionActionDelegate;
+import org.mozilla.geckoview.ContentBlocking;
+import org.mozilla.geckoview.GeckoResult;
+import org.mozilla.geckoview.GeckoRuntime;
+import org.mozilla.geckoview.GeckoRuntimeSettings;
+import org.mozilla.geckoview.GeckoSession;
+import org.mozilla.geckoview.GeckoSessionSettings;
+import org.mozilla.geckoview.GeckoView;
+import org.mozilla.geckoview.GeckoWebExecutor;
+import org.mozilla.geckoview.Image;
+import org.mozilla.geckoview.SlowScriptResponse;
+import org.mozilla.geckoview.WebExtension;
+import org.mozilla.geckoview.WebExtensionController;
+import org.mozilla.geckoview.WebNotification;
+import org.mozilla.geckoview.WebNotificationDelegate;
+import org.mozilla.geckoview.WebRequest;
+import org.mozilla.geckoview.WebRequestError;
+import org.mozilla.geckoview.RuntimeTelemetry;
+import org.mozilla.geckoview.WebResponse;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemClock;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.content.ContextCompat;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.PreferenceManager;
+import android.text.InputType;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+
+import java.io.BufferedReader;
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+interface WebExtensionDelegate {
+ default GeckoSession toggleBrowserActionPopup(boolean force) {
+ return null;
+ }
+ default void onActionButton(ActionButton button) {}
+ default TabSession getSession(GeckoSession session) {
+ return null;
+ }
+ default TabSession getCurrentSession() {
+ return null;
+ }
+ default void closeTab(TabSession session) {}
+ default void updateTab(TabSession session, WebExtension.UpdateTabDetails details) {}
+ default TabSession openNewTab(WebExtension.CreateTabDetails details) { return null; }
+}
+
+class WebExtensionManager implements WebExtension.ActionDelegate,
+ WebExtension.SessionTabDelegate,
+ WebExtension.TabDelegate,
+ WebExtensionController.PromptDelegate,
+ WebExtensionController.DebuggerDelegate,
+ TabSessionManager.TabObserver {
+ public WebExtension extension;
+
+ private LruCache<Image, Bitmap> mBitmapCache = new LruCache<>(5);
+ private GeckoRuntime mRuntime;
+ private WebExtension.Action mDefaultAction;
+ private TabSessionManager mTabManager;
+
+ private WeakReference<WebExtensionDelegate> mExtensionDelegate;
+
+ @Nullable
+ @Override
+ public GeckoResult<AllowOrDeny> onInstallPrompt(final @NonNull WebExtension extension) {
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+
+ @Nullable
+ @Override
+ public GeckoResult<AllowOrDeny> onUpdatePrompt(@NonNull WebExtension currentlyInstalled,
+ @NonNull WebExtension updatedExtension,
+ @NonNull String[] newPermissions,
+ @NonNull String[] newOrigins) {
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+
+ @Override
+ public void onExtensionListUpdated() {
+ refreshExtensionList();
+ }
+
+ // We only support either one browserAction or one pageAction
+ private void onAction(final WebExtension extension, final GeckoSession session,
+ final WebExtension.Action action) {
+ WebExtensionDelegate delegate = mExtensionDelegate.get();
+ if (delegate == null) {
+ return;
+ }
+
+ WebExtension.Action resolved;
+
+ if (session == null) {
+ // This is the default action
+ mDefaultAction = action;
+ resolved = actionFor(delegate.getCurrentSession());
+ } else {
+ if (delegate.getSession(session) == null) {
+ return;
+ }
+ delegate.getSession(session).action = action;
+ if (delegate.getCurrentSession() != session) {
+ // This update is not for the session that we are currently displaying,
+ // no need to update the UI
+ return;
+ }
+ resolved = action.withDefault(mDefaultAction);
+ }
+
+ updateAction(resolved);
+ }
+
+ @Override
+ public GeckoResult<GeckoSession> onNewTab(WebExtension source,
+ WebExtension.CreateTabDetails details) {
+ WebExtensionDelegate delegate = mExtensionDelegate.get();
+ if (delegate == null) {
+ return GeckoResult.fromValue(null);
+ }
+ return GeckoResult.fromValue(delegate.openNewTab(details));
+ }
+
+ @Override
+ public GeckoResult<AllowOrDeny> onCloseTab(WebExtension extension, GeckoSession session) {
+ final WebExtensionDelegate delegate = mExtensionDelegate.get();
+ if (delegate == null) {
+ return GeckoResult.fromValue(AllowOrDeny.DENY);
+ }
+
+ final TabSession tabSession = mTabManager.getSession(session);
+ if (tabSession != null) {
+ delegate.closeTab(tabSession);
+ }
+
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+
+ @Override
+ public GeckoResult<AllowOrDeny> onUpdateTab(WebExtension extension,
+ GeckoSession session,
+ WebExtension.UpdateTabDetails updateDetails) {
+ final WebExtensionDelegate delegate = mExtensionDelegate.get();
+ if (delegate == null) {
+ return GeckoResult.fromValue(AllowOrDeny.DENY);
+ }
+
+ final TabSession tabSession = mTabManager.getSession(session);
+ if (tabSession != null) {
+ delegate.updateTab(tabSession, updateDetails);
+ }
+
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+
+ @Override
+ public void onPageAction(final WebExtension extension,
+ final GeckoSession session,
+ final WebExtension.Action action) {
+ onAction(extension, session, action);
+ }
+
+ @Override
+ public void onBrowserAction(final WebExtension extension,
+ final GeckoSession session,
+ final WebExtension.Action action) {
+ onAction(extension, session, action);
+ }
+
+ private GeckoResult<GeckoSession> togglePopup(boolean force) {
+ WebExtensionDelegate extensionDelegate = mExtensionDelegate.get();
+ if (extensionDelegate == null) {
+ return null;
+ }
+
+ GeckoSession session = extensionDelegate.toggleBrowserActionPopup(false);
+ if (session == null) {
+ return null;
+ }
+
+ return GeckoResult.fromValue(session);
+ }
+
+ @Override
+ public GeckoResult<GeckoSession> onTogglePopup(final @NonNull WebExtension extension,
+ final @NonNull WebExtension.Action action) {
+ return togglePopup(false);
+ }
+
+ @Override
+ public GeckoResult<GeckoSession> onOpenPopup(final @NonNull WebExtension extension,
+ final @NonNull WebExtension.Action action) {
+ return togglePopup(true);
+ }
+
+ private WebExtension.Action actionFor(TabSession session) {
+ if (session.action == null) {
+ return mDefaultAction;
+ } else {
+ return session.action.withDefault(mDefaultAction);
+ }
+ }
+
+ private void updateAction(WebExtension.Action resolved) {
+ WebExtensionDelegate extensionDelegate = mExtensionDelegate.get();
+ if (extensionDelegate == null) {
+ return;
+ }
+
+ if (resolved == null || resolved.enabled == null || !resolved.enabled) {
+ extensionDelegate.onActionButton(null);
+ return;
+ }
+
+ if (resolved.icon != null) {
+ if (mBitmapCache.get(resolved.icon) != null) {
+ extensionDelegate.onActionButton(new ActionButton(
+ mBitmapCache.get(resolved.icon), resolved.badgeText,
+ resolved.badgeTextColor,
+ resolved.badgeBackgroundColor
+ ));
+ } else {
+ resolved.icon.getBitmap(100).accept(bitmap -> {
+ mBitmapCache.put(resolved.icon, bitmap);
+ extensionDelegate.onActionButton(new ActionButton(
+ bitmap, resolved.badgeText,
+ resolved.badgeTextColor,
+ resolved.badgeBackgroundColor));
+ });
+ }
+ } else {
+ extensionDelegate.onActionButton(null);
+ }
+ }
+
+ public void onClicked(TabSession session) {
+ WebExtension.Action action = actionFor(session);
+ if (action != null) {
+ action.click();
+ }
+ }
+
+ public void setExtensionDelegate(WebExtensionDelegate delegate) {
+ mExtensionDelegate = new WeakReference<>(delegate);
+ }
+
+ @Override
+ public void onCurrentSession(TabSession session) {
+ if (mDefaultAction == null) {
+ // No action was ever defined, so nothing to do
+ return;
+ }
+
+ if (session.action != null) {
+ updateAction(session.action.withDefault(mDefaultAction));
+ } else {
+ updateAction(mDefaultAction);
+ }
+ }
+
+ public GeckoResult<Void> unregisterExtension() {
+ if (extension == null) {
+ return GeckoResult.fromValue(null);
+ }
+
+ mTabManager.unregisterWebExtension();
+
+ return mRuntime.getWebExtensionController().uninstall(extension).accept((unused) -> {
+ extension = null;
+ mDefaultAction = null;
+ updateAction(null);
+ });
+ }
+
+ public GeckoResult<WebExtension> updateExtension() {
+ if (extension == null) {
+ return GeckoResult.fromValue(null);
+ }
+
+ return mRuntime.getWebExtensionController().update(extension).map(newExtension -> {
+ registerExtension(newExtension);
+ return newExtension;
+ });
+ }
+
+ public void registerExtension(WebExtension extension) {
+ extension.setActionDelegate(this);
+ extension.setTabDelegate(this);
+ mTabManager.setWebExtensionDelegates(extension, this, this);
+ this.extension = extension;
+ }
+
+ private void refreshExtensionList() {
+ mRuntime.getWebExtensionController()
+ .list().accept(extensions -> {
+ for (final WebExtension extension : extensions) {
+ registerExtension(extension);
+ }
+ });
+ }
+
+ public WebExtensionManager(GeckoRuntime runtime,
+ TabSessionManager tabManager) {
+ mTabManager = tabManager;
+ mRuntime = runtime;
+ refreshExtensionList();
+ }
+}
+
+public class GeckoViewActivity
+ extends AppCompatActivity
+ implements
+ ToolbarLayout.TabListener,
+ WebExtensionDelegate,
+ SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String LOGTAG = "GeckoViewActivity";
+ private static final String FULL_ACCESSIBILITY_TREE_EXTRA = "full_accessibility_tree";
+ private static final String SEARCH_URI_BASE = "https://www.google.com/search?q=";
+ private static final String ACTION_SHUTDOWN = "org.mozilla.geckoview_example.SHUTDOWN";
+ private static final String CHANNEL_ID = "GeckoViewExample";
+ private static final int REQUEST_FILE_PICKER = 1;
+ private static final int REQUEST_PERMISSIONS = 2;
+ private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 3;
+
+ private static GeckoRuntime sGeckoRuntime;
+
+ private static WebExtensionManager sExtensionManager;
+
+ private TabSessionManager mTabSessionManager;
+ private GeckoView mGeckoView;
+ private boolean mFullAccessibilityTree;
+ private boolean mUsePrivateBrowsing;
+ private boolean mKillProcessOnDestroy;
+ private boolean mDesktopMode;
+ private boolean mTrackingProtectionException;
+
+ private TabSession mPopupSession;
+ private View mPopupView;
+
+ private boolean mShowNotificationsRejected;
+ private ArrayList<String> mAcceptedPersistentStorage = new ArrayList<String>();
+
+ private ToolbarLayout mToolbarView;
+ private String mCurrentUri;
+ private boolean mCanGoBack;
+ private boolean mCanGoForward;
+ private boolean mFullScreen;
+
+ private HashMap<String, Integer> mNotificationIDMap = new HashMap<>();
+ private HashMap<Integer, WebNotification> mNotificationMap = new HashMap<>();
+ private int mLastID = 100;
+
+ private ProgressBar mProgressView;
+
+ private LinkedList<WebResponse> mPendingDownloads = new LinkedList<>();
+
+ private int mNextActivityResultCode = 10;
+ private HashMap<Integer, GeckoResult<Intent>> mPendingActivityResult = new HashMap<>();
+
+ private LocationView.CommitListener mCommitListener = new LocationView.CommitListener() {
+ @Override
+ public void onCommit(String text) {
+ if ((text.contains(".") || text.contains(":")) && !text.contains(" ")) {
+ mTabSessionManager.getCurrentSession().loadUri(text);
+ } else {
+ mTabSessionManager.getCurrentSession().loadUri(SEARCH_URI_BASE + text);
+ }
+ mGeckoView.requestFocus();
+ }
+ };
+
+ @Override
+ public TabSession openNewTab(WebExtension.CreateTabDetails details) {
+ final TabSession newSession = createSession(details.cookieStoreId);
+ mToolbarView.updateTabCount();
+ if (details.active == Boolean.TRUE) {
+ setGeckoViewSession(newSession, false);
+ }
+ return newSession;
+ }
+
+ private final List<Setting<?>> SETTINGS = new ArrayList<>();
+
+ private abstract class Setting<T> {
+ private int mKey;
+ private int mDefaultKey;
+ private final boolean mReloadCurrentSession;
+ private T mValue;
+
+ public Setting(final int key, final int defaultValueKey, final boolean reloadCurrentSession) {
+ mKey = key;
+ mDefaultKey = defaultValueKey;
+ mReloadCurrentSession = reloadCurrentSession;
+
+ SETTINGS.add(this);
+ }
+
+ public void onPrefChange(SharedPreferences pref) {
+ final T defaultValue = getDefaultValue(mDefaultKey, getResources());
+ final String key = getResources().getString(this.mKey);
+ final T value = getValue(key, defaultValue, pref);
+ if (!value().equals(value)) {
+ setValue(value);
+ }
+ }
+
+ private void setValue(final T newValue) {
+ mValue = newValue;
+ for (final TabSession session : mTabSessionManager.getSessions()) {
+ setValue(session.getSettings(), value());
+ }
+ if (sGeckoRuntime != null) {
+ setValue(sGeckoRuntime.getSettings(), value());
+ if (sExtensionManager != null) {
+ setValue(sGeckoRuntime.getWebExtensionController(), value());
+ }
+ }
+
+ final GeckoSession current = mTabSessionManager.getCurrentSession();
+ if (mReloadCurrentSession && current != null) {
+ current.reload();
+ }
+ }
+
+ public T value() {
+ return mValue == null ? getDefaultValue(mDefaultKey, getResources()) : mValue;
+ }
+
+ protected abstract T getDefaultValue(final int key, final Resources res);
+ protected abstract T getValue(final String key, final T defaultValue,
+ final SharedPreferences preferences);
+
+ /** Override one of these to define the behavior when this setting changes. */
+ protected void setValue(final GeckoSessionSettings settings, final T value) {}
+ protected void setValue(final GeckoRuntimeSettings settings, final T value) {}
+ protected void setValue(final WebExtensionController controller, final T value) {}
+ }
+
+ private class StringSetting extends Setting<String> {
+ public StringSetting(final int key, final int defaultValueKey) {
+ this(key, defaultValueKey, false);
+ }
+
+ public StringSetting(final int key, final int defaultValueKey,
+ final boolean reloadCurrentSession) {
+ super(key, defaultValueKey, reloadCurrentSession);
+ }
+
+ @Override
+ protected String getDefaultValue(int key, final Resources res) {
+ return res.getString(key);
+ }
+
+ @Override
+ public String getValue(final String key, final String defaultValue,
+ final SharedPreferences preferences) {
+ return preferences.getString(key, defaultValue);
+ }
+ }
+
+ private class BooleanSetting extends Setting<Boolean> {
+ public BooleanSetting(final int key, final int defaultValueKey) {
+ this(key, defaultValueKey, false);
+ }
+
+ public BooleanSetting(final int key, final int defaultValueKey,
+ final boolean reloadCurrentSession) {
+ super(key, defaultValueKey, reloadCurrentSession);
+ }
+
+ @Override
+ protected Boolean getDefaultValue(int key, Resources res) {
+ return res.getBoolean(key);
+ }
+
+ @Override
+ public Boolean getValue(final String key, final Boolean defaultValue,
+ final SharedPreferences preferences) {
+ return preferences.getBoolean(key, defaultValue);
+ }
+ }
+
+ private class IntSetting extends Setting<Integer> {
+ public IntSetting(final int key, final int defaultValueKey) {
+ this(key, defaultValueKey, false);
+ }
+
+ public IntSetting(final int key, final int defaultValueKey,
+ final boolean reloadCurrentSession) {
+ super(key, defaultValueKey, reloadCurrentSession);
+ }
+
+ @Override
+ protected Integer getDefaultValue(int key, Resources res) {
+ return res.getInteger(key);
+ }
+
+ @Override
+ public Integer getValue(final String key, final Integer defaultValue,
+ final SharedPreferences preferences) {
+ return Integer.parseInt(
+ preferences.getString(key, Integer.toString(defaultValue)));
+ }
+ }
+
+ private final IntSetting mDisplayMode = new IntSetting(
+ R.string.key_display_mode, R.integer.display_mode_default) {
+ @Override
+ public void setValue(final GeckoSessionSettings settings, final Integer value) {
+ settings.setDisplayMode(value);
+ }
+ };
+
+ private final IntSetting mPreferredColorScheme = new IntSetting(
+ R.string.key_preferred_color_scheme, R.integer.preferred_color_scheme_default,
+ /* reloadCurrentSession */ true
+ ) {
+ @Override
+ public void setValue(final GeckoRuntimeSettings settings, final Integer value) {
+ settings.setPreferredColorScheme(value);
+ }
+ };
+
+ private final StringSetting mUserAgent = new StringSetting(
+ R.string.key_user_agent_override, R.string.user_agent_override_default,
+ /* reloadCurrentSession */ true
+ ) {
+ @Override
+ public void setValue(final GeckoSessionSettings settings, final String value) {
+ settings.setUserAgentOverride(value.isEmpty() ? null : value);
+ }
+ };
+
+ private final BooleanSetting mRemoteDebugging = new BooleanSetting(
+ R.string.key_remote_debugging, R.bool.remote_debugging_default
+ ) {
+ @Override
+ public void setValue(final GeckoRuntimeSettings settings, final Boolean value) {
+ settings.setRemoteDebuggingEnabled(value);
+ }
+ };
+
+ private final BooleanSetting mJavascriptEnabled = new BooleanSetting(
+ R.string.key_javascript_enabled, R.bool.javascript_enabled_default,
+ /* reloadCurrentSession */ true
+ ) {
+ @Override
+ public void setValue(final GeckoRuntimeSettings settings, final Boolean value) {
+ settings.setJavaScriptEnabled(value);
+ }
+ };
+
+ private final BooleanSetting mTrackingProtection = new BooleanSetting(
+ R.string.key_tracking_protection, R.bool.tracking_protection_default
+ ) {
+ @Override
+ public void setValue(final GeckoRuntimeSettings settings, final Boolean value) {
+ mTabSessionManager.setUseTrackingProtection(value);
+ settings.getContentBlocking()
+ .setStrictSocialTrackingProtection(value);
+ }
+ };
+
+ private final StringSetting mEnhancedTrackingProtection = new StringSetting(
+ R.string.key_enhanced_tracking_protection, R.string.enhanced_tracking_protection_default
+ ) {
+ @Override
+ public void setValue(final GeckoRuntimeSettings settings, final String value) {
+ int etpLevel;
+ switch (value) {
+ case "disabled":
+ etpLevel = ContentBlocking.EtpLevel.NONE;
+ break;
+ case "standard":
+ etpLevel = ContentBlocking.EtpLevel.DEFAULT;
+ break;
+ case "strict":
+ etpLevel = ContentBlocking.EtpLevel.STRICT;
+ break;
+ default:
+ throw new RuntimeException("Invalid ETP level: " + value);
+ }
+
+ settings.getContentBlocking().setEnhancedTrackingProtectionLevel(etpLevel);
+ }
+ };
+
+ private final BooleanSetting mDynamicFirstPartyIsolation = new BooleanSetting(
+ R.string.key_dfpi, R.bool.dfpi_default
+ ) {
+ @Override
+ public void setValue(final GeckoRuntimeSettings settings, final Boolean value) {
+ int cookieBehavior = value ?
+ ContentBlocking.CookieBehavior.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS :
+ ContentBlocking.CookieBehavior.ACCEPT_NON_TRACKERS;
+ settings.getContentBlocking().setCookieBehavior(cookieBehavior);
+ }
+ };
+
+ private final BooleanSetting mAllowAutoplay = new BooleanSetting(
+ R.string.key_autoplay, R.bool.autoplay_default, /* reloadCurrentSession */ true
+ );
+
+ private final BooleanSetting mAllowExtensionsInPrivateBrowsing = new BooleanSetting(
+ R.string.key_allow_extensions_in_private_browsing,
+ R.bool.allow_extensions_in_private_browsing_default
+ ) {
+ @Override
+ public void setValue(final WebExtensionController controller, final Boolean value) {
+ controller.setAllowedInPrivateBrowsing(
+ sExtensionManager.extension,
+ value);
+ }
+ };
+
+ private void onPreferencesChange(SharedPreferences preferences) {
+ for (Setting<?> setting : SETTINGS) {
+ setting.onPrefChange(preferences);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
+ " - application start");
+ createNotificationChannel();
+ setContentView(R.layout.geckoview_activity);
+ mGeckoView = findViewById(R.id.gecko_view);
+
+ mTabSessionManager = new TabSessionManager();
+
+ setSupportActionBar(findViewById(R.id.toolbar));
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ preferences.registerOnSharedPreferenceChangeListener(this);
+ // Read initial preference state
+ onPreferencesChange(preferences);
+
+ mToolbarView = new ToolbarLayout(this, mTabSessionManager);
+ mToolbarView.setId(R.id.toolbar_layout);
+ mToolbarView.setTabListener(this);
+
+ getSupportActionBar().setCustomView(mToolbarView,
+ new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT,
+ ActionBar.LayoutParams.WRAP_CONTENT));
+ getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+
+ mFullAccessibilityTree = getIntent().getBooleanExtra(FULL_ACCESSIBILITY_TREE_EXTRA, false);
+ mProgressView = findViewById(R.id.page_progress);
+
+ if (sGeckoRuntime == null) {
+ final GeckoRuntimeSettings.Builder runtimeSettingsBuilder =
+ new GeckoRuntimeSettings.Builder();
+
+ if (BuildConfig.DEBUG) {
+ // In debug builds, we want to load JavaScript resources fresh with
+ // each build.
+ runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" });
+ }
+
+ final Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ runtimeSettingsBuilder.extras(extras);
+ }
+ runtimeSettingsBuilder
+ .remoteDebuggingEnabled(mRemoteDebugging.value())
+ .consoleOutput(true)
+ .contentBlocking(new ContentBlocking.Settings.Builder()
+ .antiTracking(ContentBlocking.AntiTracking.DEFAULT |
+ ContentBlocking.AntiTracking.STP)
+ .safeBrowsing(ContentBlocking.SafeBrowsing.DEFAULT)
+ .cookieBehavior(ContentBlocking.CookieBehavior.ACCEPT_NON_TRACKERS)
+ .enhancedTrackingProtectionLevel(ContentBlocking.EtpLevel.DEFAULT)
+ .build())
+ .crashHandler(ExampleCrashHandler.class)
+ .preferredColorScheme(mPreferredColorScheme.value())
+ .telemetryDelegate(new ExampleTelemetryDelegate())
+ .javaScriptEnabled(mJavascriptEnabled.value())
+ .aboutConfigEnabled(true);
+
+ sGeckoRuntime = GeckoRuntime.create(this, runtimeSettingsBuilder.build());
+
+ sExtensionManager = new WebExtensionManager(sGeckoRuntime, mTabSessionManager);
+ mTabSessionManager.setTabObserver(sExtensionManager);
+
+ sGeckoRuntime.getWebExtensionController().setDebuggerDelegate(sExtensionManager);
+
+ // `getSystemService` call requires API level 23
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ sGeckoRuntime.setWebNotificationDelegate(new WebNotificationDelegate() {
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ @Override
+ public void onShowNotification(@NonNull WebNotification notification) {
+ Intent clickIntent = new Intent(GeckoViewActivity.this, GeckoViewActivity.class);
+ clickIntent.putExtra("onClick",notification.tag);
+ PendingIntent dismissIntent = PendingIntent.getActivity(GeckoViewActivity.this, mLastID, clickIntent, 0);
+
+ Notification.Builder builder = new Notification.Builder(GeckoViewActivity.this)
+ .setContentTitle(notification.title)
+ .setContentText(notification.text)
+ .setSmallIcon(R.drawable.ic_status_logo)
+ .setContentIntent(dismissIntent)
+ .setAutoCancel(true);
+
+ mNotificationIDMap.put(notification.tag, mLastID);
+ mNotificationMap.put(mLastID, notification);
+
+ if (notification.imageUrl != null && notification.imageUrl.length() > 0) {
+ final GeckoWebExecutor executor = new GeckoWebExecutor(sGeckoRuntime);
+
+ GeckoResult<WebResponse> response = executor.fetch(
+ new WebRequest.Builder(notification.imageUrl)
+ .addHeader("Accept", "image")
+ .build());
+ response.accept(value -> {
+ Bitmap bitmap = BitmapFactory.decodeStream(value.body);
+ builder.setLargeIcon(Icon.createWithBitmap(bitmap));
+ notificationManager.notify(mLastID++, builder.build());
+ });
+ } else {
+ notificationManager.notify(mLastID++, builder.build());
+ }
+
+ }
+
+ @Override
+ public void onCloseNotification(@NonNull WebNotification notification) {
+ if (mNotificationIDMap.containsKey(notification.tag)) {
+ int id = mNotificationIDMap.get(notification.tag);
+ notificationManager.cancel(id);
+ mNotificationMap.remove(id);
+ mNotificationIDMap.remove(notification.tag);
+ }
+ }
+ });
+
+
+ }
+
+ sGeckoRuntime.setDelegate(() -> {
+ mKillProcessOnDestroy = true;
+ finish();
+ });
+
+ sGeckoRuntime.setActivityDelegate(pendingIntent -> {
+ final GeckoResult<Intent> result = new GeckoResult<>();
+ try {
+ final int code = mNextActivityResultCode++;
+ mPendingActivityResult.put(code, result);
+ GeckoViewActivity.this.startIntentSenderForResult(pendingIntent.getIntentSender(), code, null, 0, 0, 0);
+ } catch (IntentSender.SendIntentException e) {
+ result.completeExceptionally(e);
+ }
+ return result;
+ });
+ }
+
+ sExtensionManager.setExtensionDelegate(this);
+
+ if(savedInstanceState == null) {
+ TabSession session = getIntent().getParcelableExtra("session");
+ if (session != null) {
+ connectSession(session);
+
+ if (!session.isOpen()) {
+ session.open(sGeckoRuntime);
+ }
+
+ mFullAccessibilityTree = session.getSettings().getFullAccessibilityTree();
+
+ mTabSessionManager.addSession(session);
+ session.open(sGeckoRuntime);
+ setGeckoViewSession(session);
+ } else {
+ session = createSession();
+ session.open(sGeckoRuntime);
+ mTabSessionManager.setCurrentSession(session);
+ mGeckoView.setSession(session);
+ sGeckoRuntime.getWebExtensionController().setTabActive(session, true);
+ }
+ loadFromIntent(getIntent());
+ }
+
+ mGeckoView.setDynamicToolbarMaxHeight(findViewById(R.id.toolbar).getLayoutParams().height);
+
+ mToolbarView.getLocationView().setCommitListener(mCommitListener);
+ mToolbarView.updateTabCount();
+ }
+
+ private void openSettingsActivity() {
+ Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ }
+
+ @Override
+ public TabSession getSession(GeckoSession session) {
+ return mTabSessionManager.getSession(session);
+ }
+
+ @Override
+ public TabSession getCurrentSession() {
+ return mTabSessionManager.getCurrentSession();
+ }
+
+ @Override
+ public void onActionButton(ActionButton button) {
+ mToolbarView.setBrowserActionButton(button);
+ }
+
+ @Override
+ public GeckoSession toggleBrowserActionPopup(boolean force) {
+ if (mPopupSession == null) {
+ openPopupSession();
+ }
+
+ ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
+ boolean shouldShow = force || params.width == 0;
+ setPopupVisibility(shouldShow);
+
+ return shouldShow ? mPopupSession : null;
+ }
+
+ private void setPopupVisibility(boolean visible) {
+ if (mPopupView == null) {
+ return;
+ }
+
+ ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
+
+ if (visible) {
+ params.height = 1100;
+ params.width = 1200;
+ } else {
+ params.height = 0;
+ params.width = 0;
+ }
+
+ mPopupView.setLayoutParams(params);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
+ onPreferencesChange(sharedPreferences);
+ }
+
+ private class PopupSessionContentDelegate implements GeckoSession.ContentDelegate {
+ @Override
+ public void onCloseRequest(final GeckoSession session) {
+ setPopupVisibility(false);
+ if (mPopupSession != null) {
+ mPopupSession.close();
+ }
+ mPopupSession = null;
+ mPopupView = null;
+ }
+ }
+
+ private void openPopupSession() {
+ LayoutInflater inflater = (LayoutInflater)
+ getSystemService(LAYOUT_INFLATER_SERVICE);
+ mPopupView = inflater.inflate(R.layout.browser_action_popup, null);
+ GeckoView geckoView = mPopupView.findViewById(R.id.gecko_view_popup);
+ geckoView.setViewBackend(GeckoView.BACKEND_TEXTURE_VIEW);
+ mPopupSession = new TabSession();
+ mPopupSession.setContentDelegate(new PopupSessionContentDelegate());
+ mPopupSession.open(sGeckoRuntime);
+ geckoView.setSession(mPopupSession);
+
+ mPopupView.setOnFocusChangeListener(this::hideBrowserAction);
+ RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(0, 0);
+ params.addRule(RelativeLayout.ABOVE, R.id.toolbar);
+ mPopupView.setLayoutParams(params);
+ mPopupView.setFocusable(true);
+ ((ViewGroup) findViewById(R.id.main)).addView(mPopupView);
+ }
+
+ private void hideBrowserAction(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
+ params.height = 0;
+ params.width = 0;
+ mPopupView.setLayoutParams(params);
+ }
+ }
+
+ private void createNotificationChannel() {
+ // Create the NotificationChannel, but only on API 26+ because
+ // the NotificationChannel class is new and not in the support library
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ CharSequence name = getString(R.string.app_name);
+ String description = getString(R.string.activity_label);
+ int importance = NotificationManager.IMPORTANCE_DEFAULT;
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
+ channel.setDescription(description);
+ // Register the channel with the system; you can't change the importance
+ // or other notification behaviors after this
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ private TabSession createSession(final @Nullable String cookieStoreId) {
+ GeckoSessionSettings.Builder settingsBuilder = new GeckoSessionSettings.Builder();
+ settingsBuilder
+ .usePrivateMode(mUsePrivateBrowsing)
+ .fullAccessibilityTree(mFullAccessibilityTree)
+ .userAgentOverride(mUserAgent.value())
+ .viewportMode(mDesktopMode
+ ? GeckoSessionSettings.VIEWPORT_MODE_DESKTOP
+ : GeckoSessionSettings.VIEWPORT_MODE_MOBILE)
+ .userAgentMode(mDesktopMode
+ ? GeckoSessionSettings.USER_AGENT_MODE_DESKTOP
+ : GeckoSessionSettings.USER_AGENT_MODE_MOBILE)
+ .useTrackingProtection(mTrackingProtection.value())
+ .displayMode(mDisplayMode.value());
+
+ if (cookieStoreId != null) {
+ settingsBuilder.contextId(cookieStoreId);
+ }
+
+ TabSession session = mTabSessionManager.newSession(settingsBuilder.build());
+ connectSession(session);
+
+ return session;
+ }
+
+ private TabSession createSession() {
+ return createSession(null);
+ }
+
+ private void connectSession(GeckoSession session) {
+ session.setContentDelegate(new ExampleContentDelegate());
+ session.setHistoryDelegate(new ExampleHistoryDelegate());
+ final ExampleContentBlockingDelegate cb = new ExampleContentBlockingDelegate();
+ session.setContentBlockingDelegate(cb);
+ session.setProgressDelegate(new ExampleProgressDelegate(cb));
+ session.setNavigationDelegate(new ExampleNavigationDelegate());
+
+ final BasicGeckoViewPrompt prompt = new BasicGeckoViewPrompt(this);
+ prompt.filePickerRequestCode = REQUEST_FILE_PICKER;
+ session.setPromptDelegate(prompt);
+
+ final ExamplePermissionDelegate permission = new ExamplePermissionDelegate();
+ permission.androidPermissionRequestCode = REQUEST_PERMISSIONS;
+ session.setPermissionDelegate(permission);
+
+ session.setMediaDelegate(new ExampleMediaDelegate(this));
+
+ session.setSelectionActionDelegate(new BasicSelectionActionDelegate(this));
+ if (sExtensionManager.extension != null) {
+ final WebExtension.SessionController sessionController =
+ session.getWebExtensionController();
+ sessionController.setActionDelegate(sExtensionManager.extension, sExtensionManager);
+ sessionController.setTabDelegate(sExtensionManager.extension, sExtensionManager);
+ }
+
+ updateDesktopMode(session);
+ }
+
+ private void recreateSession() {
+ recreateSession(mTabSessionManager.getCurrentSession());
+ }
+
+ private void recreateSession(TabSession session) {
+ if (session != null) {
+ mTabSessionManager.closeSession(session);
+ }
+
+ session = createSession();
+ session.open(sGeckoRuntime);
+ mTabSessionManager.setCurrentSession(session);
+ mGeckoView.setSession(session);
+ sGeckoRuntime.getWebExtensionController().setTabActive(session, true);
+ if (mCurrentUri != null) {
+ session.loadUri(mCurrentUri);
+ }
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState != null) {
+ mTabSessionManager.setCurrentSession((TabSession) mGeckoView.getSession());
+ sGeckoRuntime.getWebExtensionController().setTabActive(mGeckoView.getSession(), true);
+ } else {
+ recreateSession();
+ }
+ }
+
+ private void updateDesktopMode(GeckoSession session) {
+ session.getSettings().setViewportMode(mDesktopMode
+ ? GeckoSessionSettings.VIEWPORT_MODE_DESKTOP
+ : GeckoSessionSettings.VIEWPORT_MODE_MOBILE);
+ session.getSettings().setUserAgentMode(mDesktopMode
+ ? GeckoSessionSettings.USER_AGENT_MODE_DESKTOP
+ : GeckoSessionSettings.USER_AGENT_MODE_MOBILE);
+ }
+
+ @Override
+ public void onBackPressed() {
+ GeckoSession session = mTabSessionManager.getCurrentSession();
+ if (mFullScreen && session != null) {
+ session.exitFullScreen();
+ return;
+ }
+
+ if (mCanGoBack && session != null) {
+ session.goBack();
+ return;
+ }
+
+ super.onBackPressed();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.actions, menu);
+ return true;
+ }
+
+ private void updateTrackingProtectionException() {
+ if (sGeckoRuntime == null) {
+ return;
+ }
+
+ final GeckoSession session = mTabSessionManager.getCurrentSession();
+ if (session == null) {
+ return;
+ }
+
+ sGeckoRuntime.getContentBlockingController()
+ .checkException(session)
+ .accept(value -> mTrackingProtectionException = value.booleanValue());
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(R.id.action_pb).setChecked(mUsePrivateBrowsing);
+ menu.findItem(R.id.desktop_mode).setChecked(mDesktopMode);
+ menu.findItem(R.id.action_tpe).setChecked(mTrackingProtectionException);
+ menu.findItem(R.id.action_forward).setEnabled(mCanGoForward);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ GeckoSession session = mTabSessionManager.getCurrentSession();
+ switch (item.getItemId()) {
+ case R.id.action_reload:
+ session.reload();
+ break;
+ case R.id.action_forward:
+ session.goForward();
+ break;
+ case R.id.action_tpe:
+ sGeckoRuntime.getContentBlockingController().checkException(session).accept(value -> {
+ if (value.booleanValue()) {
+ sGeckoRuntime.getContentBlockingController().removeException(session);
+ } else {
+ sGeckoRuntime.getContentBlockingController().addException(session);
+ }
+ session.reload();
+ });
+ break;
+ case R.id.desktop_mode:
+ mDesktopMode = !mDesktopMode;
+ updateDesktopMode(session);
+ session.reload();
+ break;
+ case R.id.action_pb:
+ mUsePrivateBrowsing = !mUsePrivateBrowsing;
+ recreateSession();
+ break;
+ case R.id.install_addon:
+ installAddon();
+ break;
+ case R.id.update_addon:
+ updateAddon();
+ break;
+ case R.id.settings:
+ openSettingsActivity();
+ break;
+ case R.id.action_new_tab:
+ createNewTab();
+ break;
+ case R.id.action_close_tab:
+ closeTab((TabSession)session);
+ break;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+
+ return true;
+ }
+
+ private void installAddon() {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.install_addon);
+
+ final EditText input = new EditText(this);
+ input.setInputType(InputType.TYPE_CLASS_TEXT);
+ input.setHint(R.string.install_addon_hint);
+ builder.setView(input);
+
+ builder.setPositiveButton(R.string.install, (dialog, which) -> {
+ final String uri = input.getText().toString();
+
+ // We only suopport one extension at a time, so remove the currently installed
+ // extension if there is one
+ setPopupVisibility(false);
+ mPopupView = null;
+ mPopupSession = null;
+ sExtensionManager.unregisterExtension().then(unused -> {
+ final WebExtensionController controller = sGeckoRuntime.getWebExtensionController();
+ controller.setPromptDelegate(sExtensionManager);
+ return controller.install(uri);
+ }).then(extension ->
+ sGeckoRuntime.getWebExtensionController().setAllowedInPrivateBrowsing(
+ extension, mAllowExtensionsInPrivateBrowsing.value())
+ ).accept(extension ->
+ sExtensionManager.registerExtension(extension));
+ });
+ builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
+ // Nothing to do
+ });
+
+ builder.show();
+ }
+
+ private void updateAddon() {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.update_addon);
+
+ sExtensionManager.updateExtension().accept(extension -> {
+ if (extension != null) {
+ builder.setMessage("Success");
+ } else {
+ builder.setMessage("No addon to update");
+ }
+ builder.show();
+ }, exception -> {
+ builder.setMessage("Failed: " + exception);
+ builder.show();
+ });
+ }
+
+ private void createNewTab() {
+ Double startTime = sGeckoRuntime.getProfilerController().getProfilerTime();
+ TabSession newSession = createSession();
+ newSession.open(sGeckoRuntime);
+ setGeckoViewSession(newSession);
+ mToolbarView.updateTabCount();
+ sGeckoRuntime.getProfilerController().addMarker("Create new tab", startTime);
+ }
+
+ @Override
+ public void closeTab(TabSession session) {
+ if (mTabSessionManager.sessionCount() > 1) {
+ mTabSessionManager.closeSession(session);
+ TabSession tabSession = mTabSessionManager.getCurrentSession();
+ setGeckoViewSession(tabSession);
+ tabSession.reload();
+ mToolbarView.updateTabCount();
+ } else {
+ recreateSession(session);
+ }
+ }
+
+ @Override
+ public void updateTab(TabSession session, WebExtension.UpdateTabDetails details) {
+ if (details.active == Boolean.TRUE) {
+ switchToSession(session, false);
+ }
+ }
+
+ public void onBrowserActionClick() {
+ sExtensionManager.onClicked(mTabSessionManager.getCurrentSession());
+ }
+
+ public void switchToSession(TabSession session, boolean activateTab) {
+ TabSession currentSession = mTabSessionManager.getCurrentSession();
+ if (session != currentSession) {
+ setGeckoViewSession(session, activateTab);
+ mCurrentUri = session.getUri();
+ if (!session.isOpen()) {
+ // Session's process was previously killed; reopen
+ session.open(sGeckoRuntime);
+ session.loadUri(mCurrentUri);
+ }
+ mToolbarView.getLocationView().setText(mCurrentUri);
+ }
+ }
+
+ public void switchToTab(int index) {
+ TabSession nextSession = mTabSessionManager.getSession(index);
+ switchToSession(nextSession, true);
+ }
+
+ private void setGeckoViewSession(TabSession session) {
+ setGeckoViewSession(session, true);
+ }
+
+ private void setGeckoViewSession(TabSession session, boolean activateTab) {
+ final WebExtensionController controller = sGeckoRuntime.getWebExtensionController();
+ final GeckoSession previousSession = mGeckoView.releaseSession();
+ if (previousSession != null) {
+ controller.setTabActive(previousSession, false);
+ }
+ mGeckoView.setSession(session);
+ if (activateTab) {
+ controller.setTabActive(session, true);
+ }
+ mTabSessionManager.setCurrentSession(session);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mKillProcessOnDestroy) {
+ android.os.Process.killProcess(android.os.Process.myPid());
+ }
+
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onNewIntent(final Intent intent) {
+ super.onNewIntent(intent);
+
+ if (ACTION_SHUTDOWN.equals(intent.getAction())) {
+ mKillProcessOnDestroy = true;
+ if (sGeckoRuntime != null) {
+ sGeckoRuntime.shutdown();
+ }
+ finish();
+ return;
+ }
+
+ if (intent.hasExtra("onClick")) {
+ int key = intent.getExtras().getInt("onClick");
+ WebNotification notification = mNotificationMap.get(key);
+ if (notification != null) {
+ notification.click();
+ mNotificationMap.remove(key);
+ }
+ }
+
+ setIntent(intent);
+
+ if (intent.getData() != null) {
+ loadFromIntent(intent);
+ }
+ }
+
+
+ private void loadFromIntent(final Intent intent) {
+ final Uri uri = intent.getData();
+ if (uri != null) {
+ mTabSessionManager.getCurrentSession().load(
+ new GeckoSession.Loader()
+ .uri(uri.toString())
+ .flags(GeckoSession.LOAD_FLAGS_EXTERNAL));
+ }
+ }
+
+ @Override
+ protected void onActivityResult(final int requestCode, final int resultCode,
+ final Intent data) {
+ if (requestCode == REQUEST_FILE_PICKER) {
+ final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt)
+ mTabSessionManager.getCurrentSession().getPromptDelegate();
+ prompt.onFileCallbackResult(resultCode, data);
+ } else if (mPendingActivityResult.containsKey(requestCode)) {
+ final GeckoResult<Intent> result = mPendingActivityResult.remove(requestCode);
+
+ if (resultCode == Activity.RESULT_OK) {
+ result.complete(data);
+ } else {
+ result.completeExceptionally(new RuntimeException("Unknown error"));
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(final int requestCode,
+ final String[] permissions,
+ final int[] grantResults) {
+ if (requestCode == REQUEST_PERMISSIONS) {
+ final ExamplePermissionDelegate permission = (ExamplePermissionDelegate)
+ mTabSessionManager.getCurrentSession().getPermissionDelegate();
+ permission.onRequestPermissionsResult(permissions, grantResults);
+ } else if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ continueDownloads();
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void continueDownloads() {
+ final LinkedList<WebResponse> downloads = mPendingDownloads;
+ mPendingDownloads = new LinkedList<>();
+
+ for (final WebResponse response : downloads) {
+ downloadFile(response);
+ }
+ }
+
+ private void downloadFile(final WebResponse response) {
+ if (response.body == null) {
+ return;
+ }
+
+ if (ContextCompat.checkSelfPermission(GeckoViewActivity.this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ mPendingDownloads.add(response);
+ ActivityCompat.requestPermissions(GeckoViewActivity.this,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ REQUEST_WRITE_EXTERNAL_STORAGE);
+ return;
+ }
+
+ final String filename = getFileName(response);
+
+ try {
+ String downloadsPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ .getAbsolutePath() + "/" + filename;
+
+ int bufferSize = 1024; // to read in 1Mb increments
+ byte[] buffer = new byte[bufferSize];
+ try (OutputStream out = new BufferedOutputStream(new FileOutputStream(downloadsPath))) {
+ int len;
+ while ( (len = response.body.read(buffer)) != -1 ) {
+ out.write(buffer, 0, len);
+ }
+ } catch (Throwable e) {
+ Log.i(LOGTAG, String.valueOf(e.getStackTrace()));
+ }
+ } catch (Throwable e) {
+ Log.i(LOGTAG, String.valueOf(e.getStackTrace()));
+ }
+ }
+
+ private String getFileName(final WebResponse response) {
+ String filename;
+ String contentDispositionHeader;
+ if (response.headers.containsKey("content-disposition")) {
+ contentDispositionHeader = response.headers.get("content-disposition");
+ } else {
+ contentDispositionHeader = response.headers.getOrDefault("Content-Disposition", "default filename=GVDownload");
+ }
+ Pattern pattern = Pattern.compile("(filename=\"?)(.+)(\"?)");
+ Matcher matcher = pattern.matcher(contentDispositionHeader);
+ if (matcher.find()) {
+ filename = matcher.group(2).replaceAll("\\s", "%20");
+ } else {
+ filename = "GVEdownload";
+ }
+
+ return filename;
+ }
+
+ private static boolean isForeground() {
+ final ActivityManager.RunningAppProcessInfo appProcessInfo = new ActivityManager.RunningAppProcessInfo();
+ ActivityManager.getMyMemoryState(appProcessInfo);
+ return appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND ||
+ appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
+ }
+
+ private String mErrorTemplate;
+ private String createErrorPage(final String error) {
+ if (mErrorTemplate == null) {
+ InputStream stream = null;
+ BufferedReader reader = null;
+ StringBuilder builder = new StringBuilder();
+ try {
+ stream = getResources().getAssets().open("error.html");
+ reader = new BufferedReader(new InputStreamReader(stream));
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ builder.append("\n");
+ }
+
+ mErrorTemplate = builder.toString();
+ } catch (IOException e) {
+ Log.d(LOGTAG, "Failed to open error page template", e);
+ return null;
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Failed to close error page template stream", e);
+ }
+ }
+
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Failed to close error page template reader", e);
+ }
+ }
+ }
+ }
+
+ return mErrorTemplate.replace("$ERROR", error);
+ }
+
+ private class ExampleHistoryDelegate implements GeckoSession.HistoryDelegate {
+ private final HashSet<String> mVisitedURLs;
+
+ private ExampleHistoryDelegate() {
+ mVisitedURLs = new HashSet<String>();
+ }
+
+ @Override
+ public GeckoResult<Boolean> onVisited(GeckoSession session, String url,
+ String lastVisitedURL, int flags) {
+ Log.i(LOGTAG, "Visited URL: " + url);
+
+ mVisitedURLs.add(url);
+ return GeckoResult.fromValue(true);
+ }
+
+ @Override
+ public GeckoResult<boolean[]> getVisited(GeckoSession session, String[] urls) {
+ boolean[] visited = new boolean[urls.length];
+ for (int i = 0; i < urls.length; i++) {
+ visited[i] = mVisitedURLs.contains(urls[i]);
+ }
+ return GeckoResult.fromValue(visited);
+ }
+
+ @Override
+ public void onHistoryStateChange(final GeckoSession session,
+ final GeckoSession.HistoryDelegate.HistoryList state) {
+ Log.i(LOGTAG, "History state updated");
+ }
+ }
+
+ private class ExampleContentDelegate implements GeckoSession.ContentDelegate {
+ @Override
+ public void onTitleChange(GeckoSession session, String title) {
+ Log.i(LOGTAG, "Content title changed to " + title);
+ TabSession tabSession = mTabSessionManager.getSession(session);
+ if (tabSession != null ) {
+ tabSession.setTitle(title);
+ }
+ }
+
+ @Override
+ public void onFullScreen(final GeckoSession session, final boolean fullScreen) {
+ getWindow().setFlags(fullScreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ mFullScreen = fullScreen;
+ if (fullScreen) {
+ getSupportActionBar().hide();
+ } else {
+ getSupportActionBar().show();
+ }
+ }
+
+ @Override
+ public void onFocusRequest(final GeckoSession session) {
+ Log.i(LOGTAG, "Content requesting focus");
+ }
+
+ @Override
+ public void onCloseRequest(final GeckoSession session) {
+ if (session == mTabSessionManager.getCurrentSession()) {
+ finish();
+ }
+ }
+
+ @Override
+ public void onContextMenu(final GeckoSession session,
+ int screenX, int screenY,
+ final ContextElement element) {
+ Log.d(LOGTAG, "onContextMenu screenX=" + screenX +
+ " screenY=" + screenY +
+ " type=" + element.type +
+ " linkUri=" + element.linkUri +
+ " title=" + element.title +
+ " alt=" + element.altText +
+ " srcUri=" + element.srcUri);
+ }
+
+ @Override
+ public void onExternalResponse(@NonNull GeckoSession session, @NonNull WebResponse response) {
+ downloadFile(response);
+ }
+
+ @Override
+ public void onCrash(GeckoSession session) {
+ Log.e(LOGTAG, "Crashed, reopening session");
+ session.open(sGeckoRuntime);
+ }
+
+ @Override
+ public void onKill(GeckoSession session) {
+ TabSession tabSession = mTabSessionManager.getSession(session);
+ if (tabSession == null) {
+ return;
+ }
+
+ if (tabSession != mTabSessionManager.getCurrentSession()) {
+ Log.e(LOGTAG, "Background session killed");
+ return;
+ }
+
+ if (isForeground()) {
+ throw new IllegalStateException("Foreground content process unexpectedly killed by OS!");
+ }
+
+ Log.e(LOGTAG, "Current session killed, reopening");
+
+ tabSession.open(sGeckoRuntime);
+ tabSession.loadUri(tabSession.getUri());
+ }
+
+ @Override
+ public void onFirstComposite(final GeckoSession session) {
+ Log.d(LOGTAG, "onFirstComposite");
+ }
+
+ @Override
+ public void onWebAppManifest(final GeckoSession session, JSONObject manifest) {
+ Log.d(LOGTAG, "onWebAppManifest: " + manifest);
+ }
+
+ private boolean activeAlert = false;
+
+ @Override
+ public GeckoResult<SlowScriptResponse> onSlowScript(final GeckoSession geckoSession,
+ final String scriptFileName) {
+ BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt) mTabSessionManager.getCurrentSession().getPromptDelegate();
+ if (prompt != null) {
+ GeckoResult<SlowScriptResponse> result = new GeckoResult<SlowScriptResponse>();
+ if (!activeAlert) {
+ activeAlert = true;
+ prompt.onSlowScriptPrompt(geckoSession, getString(R.string.slow_script), result);
+ }
+ return result.then(value -> {
+ activeAlert = false;
+ return GeckoResult.fromValue(value);
+ });
+ }
+ return null;
+ }
+
+ @Override
+ public void onMetaViewportFitChange(final GeckoSession session, final String viewportFit) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ return;
+ }
+ WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
+ if (viewportFit.equals("cover")) {
+ layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+ } else if (viewportFit.equals("contain")) {
+ layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+ } else {
+ layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
+ }
+ getWindow().setAttributes(layoutParams);
+ }
+ }
+
+ private class ExampleProgressDelegate implements GeckoSession.ProgressDelegate {
+ private ExampleContentBlockingDelegate mCb;
+
+ private ExampleProgressDelegate(final ExampleContentBlockingDelegate cb) {
+ mCb = cb;
+ }
+
+ @Override
+ public void onPageStart(GeckoSession session, String url) {
+ Log.i(LOGTAG, "Starting to load page at " + url);
+ Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
+ " - page load start");
+ mCb.clearCounters();
+ }
+
+ @Override
+ public void onPageStop(GeckoSession session, boolean success) {
+ Log.i(LOGTAG, "Stopping page load " + (success ? "successfully" : "unsuccessfully"));
+ Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
+ " - page load stop");
+ mCb.logCounters();
+ }
+
+ @Override
+ public void onProgressChange(GeckoSession session, int progress) {
+ Log.i(LOGTAG, "onProgressChange " + progress);
+
+ mProgressView.setProgress(progress);
+
+ if (progress > 0 && progress < 100) {
+ mProgressView.setVisibility(View.VISIBLE);
+ } else {
+ mProgressView.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) {
+ Log.i(LOGTAG, "Security status changed to " + securityInfo.securityMode);
+ }
+
+ @Override
+ public void onSessionStateChange(GeckoSession session, GeckoSession.SessionState state) {
+ Log.i(LOGTAG, "New Session state: " + state.toString());
+ }
+ }
+
+ private class ExamplePermissionDelegate implements GeckoSession.PermissionDelegate {
+
+ public int androidPermissionRequestCode = 1;
+ private Callback mCallback;
+
+ class ExampleNotificationCallback implements GeckoSession.PermissionDelegate.Callback {
+ private final GeckoSession.PermissionDelegate.Callback mCallback;
+ ExampleNotificationCallback(final GeckoSession.PermissionDelegate.Callback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void reject() {
+ mShowNotificationsRejected = true;
+ mCallback.reject();
+ }
+
+ @Override
+ public void grant() {
+ mShowNotificationsRejected = false;
+ mCallback.grant();
+ }
+ }
+
+ class ExamplePersistentStorageCallback implements GeckoSession.PermissionDelegate.Callback {
+ private final GeckoSession.PermissionDelegate.Callback mCallback;
+ private final String mUri;
+ ExamplePersistentStorageCallback(final GeckoSession.PermissionDelegate.Callback callback, String uri) {
+ mCallback = callback;
+ mUri = uri;
+ }
+
+ @Override
+ public void reject() {
+ mCallback.reject();
+ }
+
+ @Override
+ public void grant() {
+ mAcceptedPersistentStorage.add(mUri);
+ mCallback.grant();
+ }
+ }
+
+ public void onRequestPermissionsResult(final String[] permissions,
+ final int[] grantResults) {
+ if (mCallback == null) {
+ return;
+ }
+
+ final Callback cb = mCallback;
+ mCallback = null;
+ for (final int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ // At least one permission was not granted.
+ cb.reject();
+ return;
+ }
+ }
+ cb.grant();
+ }
+
+ @Override
+ public void onAndroidPermissionsRequest(final GeckoSession session, final String[] permissions,
+ final Callback callback) {
+ if (Build.VERSION.SDK_INT >= 23) {
+ // requestPermissions was introduced in API 23.
+ mCallback = callback;
+ requestPermissions(permissions, androidPermissionRequestCode);
+ } else {
+ callback.grant();
+ }
+ }
+
+ @Override
+ public void onContentPermissionRequest(final GeckoSession session, final String uri,
+ final int type, final Callback callback) {
+ final int resId;
+ Callback contentPermissionCallback = callback;
+ if (PERMISSION_GEOLOCATION == type) {
+ resId = R.string.request_geolocation;
+ } else if (PERMISSION_DESKTOP_NOTIFICATION == type) {
+ if (mShowNotificationsRejected) {
+ Log.w(LOGTAG, "Desktop notifications already denied by user.");
+ callback.reject();
+ return;
+ }
+ resId = R.string.request_notification;
+ contentPermissionCallback = new ExampleNotificationCallback(callback);
+ } else if (PERMISSION_PERSISTENT_STORAGE == type) {
+ if (mAcceptedPersistentStorage.contains(uri)) {
+ Log.w(LOGTAG, "Persistent Storage for " + uri + " already granted by user.");
+ callback.grant();
+ return;
+ }
+ resId = R.string.request_storage;
+ contentPermissionCallback = new ExamplePersistentStorageCallback(callback, uri);
+ } else if (PERMISSION_XR == type) {
+ resId = R.string.request_xr;
+ } else if (PERMISSION_AUTOPLAY_AUDIBLE == type || PERMISSION_AUTOPLAY_INAUDIBLE == type) {
+ if (!mAllowAutoplay.value()) {
+ Log.d(LOGTAG, "Rejecting autoplay request");
+ callback.reject();
+ } else {
+ Log.d(LOGTAG, "Granting autoplay request");
+ callback.grant();
+ }
+ return;
+ } else if (PERMISSION_MEDIA_KEY_SYSTEM_ACCESS == type) {
+ resId = R.string.request_media_key_system_access;
+ } else {
+ Log.w(LOGTAG, "Unknown permission: " + type);
+ callback.reject();
+ return;
+ }
+
+ final String title = getString(resId, Uri.parse(uri).getAuthority());
+ final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt)
+ mTabSessionManager.getCurrentSession().getPromptDelegate();
+ prompt.onPermissionPrompt(session, title, contentPermissionCallback);
+ }
+
+ private String[] normalizeMediaName(final MediaSource[] sources) {
+ if (sources == null) {
+ return null;
+ }
+
+ String[] res = new String[sources.length];
+ for (int i = 0; i < sources.length; i++) {
+ final int mediaSource = sources[i].source;
+ final String name = sources[i].name;
+ if (MediaSource.SOURCE_CAMERA == mediaSource) {
+ if (name.toLowerCase(Locale.ROOT).contains("front")) {
+ res[i] = getString(R.string.media_front_camera);
+ } else {
+ res[i] = getString(R.string.media_back_camera);
+ }
+ } else if (!name.isEmpty()) {
+ res[i] = name;
+ } else if (MediaSource.SOURCE_MICROPHONE == mediaSource) {
+ res[i] = getString(R.string.media_microphone);
+ } else {
+ res[i] = getString(R.string.media_other);
+ }
+ }
+
+ return res;
+ }
+
+ @Override
+ public void onMediaPermissionRequest(final GeckoSession session, final String uri,
+ final MediaSource[] video, final MediaSource[] audio,
+ final MediaCallback callback) {
+ // If we don't have device permissions at this point, just automatically reject the request
+ // as we will have already have requested device permissions before getting to this point
+ // and if we've reached here and we don't have permissions then that means that the user
+ // denied them.
+ if ((audio != null
+ && ContextCompat.checkSelfPermission(GeckoViewActivity.this,
+ Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED)
+ || (video != null
+ && ContextCompat.checkSelfPermission(GeckoViewActivity.this,
+ Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)) {
+ callback.reject();
+ return;
+ }
+
+ final String host = Uri.parse(uri).getAuthority();
+ final String title;
+ if (audio == null) {
+ title = getString(R.string.request_video, host);
+ } else if (video == null) {
+ title = getString(R.string.request_audio, host);
+ } else {
+ title = getString(R.string.request_media, host);
+ }
+
+ String[] videoNames = normalizeMediaName(video);
+ String[] audioNames = normalizeMediaName(audio);
+
+ final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt)
+ mTabSessionManager.getCurrentSession().getPromptDelegate();
+ prompt.onMediaPrompt(session, title, video, audio, videoNames, audioNames, callback);
+ }
+ }
+
+ private class ExampleNavigationDelegate implements GeckoSession.NavigationDelegate {
+ @Override
+ public void onLocationChange(GeckoSession session, final String url) {
+ mToolbarView.getLocationView().setText(url);
+ TabSession tabSession = mTabSessionManager.getSession(session);
+ if (tabSession != null) {
+ tabSession.onLocationChange(url);
+ }
+ mCurrentUri = url;
+ updateTrackingProtectionException();
+ }
+
+ @Override
+ public void onCanGoBack(GeckoSession session, boolean canGoBack) {
+ mCanGoBack = canGoBack;
+ }
+
+ @Override
+ public void onCanGoForward(GeckoSession session, boolean canGoForward) {
+ mCanGoForward = canGoForward;
+ }
+
+ @Override
+ public GeckoResult<AllowOrDeny> onLoadRequest(final GeckoSession session,
+ final LoadRequest request) {
+ Log.d(LOGTAG, "onLoadRequest=" + request.uri +
+ " triggerUri=" + request.triggerUri +
+ " where=" + request.target +
+ " isRedirect=" + request.isRedirect +
+ " isDirectNavigation=" + request.isDirectNavigation);
+
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+
+ @Override
+ public GeckoResult<AllowOrDeny> onSubframeLoadRequest(final GeckoSession session,
+ final LoadRequest request) {
+ Log.d(LOGTAG, "onSubframeLoadRequest=" + request.uri +
+ " triggerUri=" + request.triggerUri +
+ " isRedirect=" + request.isRedirect +
+ "isDirectNavigation=" + request.isDirectNavigation);
+
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW);
+ }
+
+ @Override
+ public GeckoResult<GeckoSession> onNewSession(final GeckoSession session, final String uri) {
+ final TabSession newSession = createSession();
+ mToolbarView.updateTabCount();
+ setGeckoViewSession(newSession);
+ // A reference to newSession is stored by mTabSessionManager,
+ // which prevents the session from being garbage-collected.
+ return GeckoResult.fromValue(newSession);
+ }
+
+ private String categoryToString(final int category) {
+ switch (category) {
+ case WebRequestError.ERROR_CATEGORY_UNKNOWN:
+ return "ERROR_CATEGORY_UNKNOWN";
+ case WebRequestError.ERROR_CATEGORY_SECURITY:
+ return "ERROR_CATEGORY_SECURITY";
+ case WebRequestError.ERROR_CATEGORY_NETWORK:
+ return "ERROR_CATEGORY_NETWORK";
+ case WebRequestError.ERROR_CATEGORY_CONTENT:
+ return "ERROR_CATEGORY_CONTENT";
+ case WebRequestError.ERROR_CATEGORY_URI:
+ return "ERROR_CATEGORY_URI";
+ case WebRequestError.ERROR_CATEGORY_PROXY:
+ return "ERROR_CATEGORY_PROXY";
+ case WebRequestError.ERROR_CATEGORY_SAFEBROWSING:
+ return "ERROR_CATEGORY_SAFEBROWSING";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ private String errorToString(final int error) {
+ switch (error) {
+ case WebRequestError.ERROR_UNKNOWN:
+ return "ERROR_UNKNOWN";
+ case WebRequestError.ERROR_SECURITY_SSL:
+ return "ERROR_SECURITY_SSL";
+ case WebRequestError.ERROR_SECURITY_BAD_CERT:
+ return "ERROR_SECURITY_BAD_CERT";
+ case WebRequestError.ERROR_NET_RESET:
+ return "ERROR_NET_RESET";
+ case WebRequestError.ERROR_NET_INTERRUPT:
+ return "ERROR_NET_INTERRUPT";
+ case WebRequestError.ERROR_NET_TIMEOUT:
+ return "ERROR_NET_TIMEOUT";
+ case WebRequestError.ERROR_CONNECTION_REFUSED:
+ return "ERROR_CONNECTION_REFUSED";
+ case WebRequestError.ERROR_UNKNOWN_PROTOCOL:
+ return "ERROR_UNKNOWN_PROTOCOL";
+ case WebRequestError.ERROR_UNKNOWN_HOST:
+ return "ERROR_UNKNOWN_HOST";
+ case WebRequestError.ERROR_UNKNOWN_SOCKET_TYPE:
+ return "ERROR_UNKNOWN_SOCKET_TYPE";
+ case WebRequestError.ERROR_UNKNOWN_PROXY_HOST:
+ return "ERROR_UNKNOWN_PROXY_HOST";
+ case WebRequestError.ERROR_MALFORMED_URI:
+ return "ERROR_MALFORMED_URI";
+ case WebRequestError.ERROR_REDIRECT_LOOP:
+ return "ERROR_REDIRECT_LOOP";
+ case WebRequestError.ERROR_SAFEBROWSING_PHISHING_URI:
+ return "ERROR_SAFEBROWSING_PHISHING_URI";
+ case WebRequestError.ERROR_SAFEBROWSING_MALWARE_URI:
+ return "ERROR_SAFEBROWSING_MALWARE_URI";
+ case WebRequestError.ERROR_SAFEBROWSING_UNWANTED_URI:
+ return "ERROR_SAFEBROWSING_UNWANTED_URI";
+ case WebRequestError.ERROR_SAFEBROWSING_HARMFUL_URI:
+ return "ERROR_SAFEBROWSING_HARMFUL_URI";
+ case WebRequestError.ERROR_CONTENT_CRASHED:
+ return "ERROR_CONTENT_CRASHED";
+ case WebRequestError.ERROR_OFFLINE:
+ return "ERROR_OFFLINE";
+ case WebRequestError.ERROR_PORT_BLOCKED:
+ return "ERROR_PORT_BLOCKED";
+ case WebRequestError.ERROR_PROXY_CONNECTION_REFUSED:
+ return "ERROR_PROXY_CONNECTION_REFUSED";
+ case WebRequestError.ERROR_FILE_NOT_FOUND:
+ return "ERROR_FILE_NOT_FOUND";
+ case WebRequestError.ERROR_FILE_ACCESS_DENIED:
+ return "ERROR_FILE_ACCESS_DENIED";
+ case WebRequestError.ERROR_INVALID_CONTENT_ENCODING:
+ return "ERROR_INVALID_CONTENT_ENCODING";
+ case WebRequestError.ERROR_UNSAFE_CONTENT_TYPE:
+ return "ERROR_UNSAFE_CONTENT_TYPE";
+ case WebRequestError.ERROR_CORRUPTED_CONTENT:
+ return "ERROR_CORRUPTED_CONTENT";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ private String createErrorPage(final int category, final int error) {
+ if (mErrorTemplate == null) {
+ InputStream stream = null;
+ BufferedReader reader = null;
+ StringBuilder builder = new StringBuilder();
+ try {
+ stream = getResources().getAssets().open("error.html");
+ reader = new BufferedReader(new InputStreamReader(stream));
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ builder.append("\n");
+ }
+
+ mErrorTemplate = builder.toString();
+ } catch (IOException e) {
+ Log.d(LOGTAG, "Failed to open error page template", e);
+ return null;
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Failed to close error page template stream", e);
+ }
+ }
+
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Failed to close error page template reader", e);
+ }
+ }
+ }
+ }
+
+ return GeckoViewActivity.this.createErrorPage(categoryToString(category) + " : " + errorToString(error));
+ }
+
+ @Override
+ public GeckoResult<String> onLoadError(final GeckoSession session, final String uri,
+ final WebRequestError error) {
+ Log.d(LOGTAG, "onLoadError=" + uri +
+ " error category=" + error.category +
+ " error=" + error.code);
+
+ return GeckoResult.fromValue("data:text/html," + createErrorPage(error.category, error.code));
+ }
+ }
+
+ private class ExampleContentBlockingDelegate
+ implements ContentBlocking.Delegate {
+ private int mBlockedAds = 0;
+ private int mBlockedAnalytics = 0;
+ private int mBlockedSocial = 0;
+ private int mBlockedContent = 0;
+ private int mBlockedTest = 0;
+ private int mBlockedStp = 0;
+
+ private void clearCounters() {
+ mBlockedAds = 0;
+ mBlockedAnalytics = 0;
+ mBlockedSocial = 0;
+ mBlockedContent = 0;
+ mBlockedTest = 0;
+ mBlockedStp = 0;
+ }
+
+ private void logCounters() {
+ Log.d(LOGTAG, "Trackers blocked: " + mBlockedAds + " ads, " +
+ mBlockedAnalytics + " analytics, " +
+ mBlockedSocial + " social, " +
+ mBlockedContent + " content, " +
+ mBlockedTest + " test, " +
+ mBlockedStp + "stp");
+ }
+
+ @Override
+ public void onContentBlocked(final GeckoSession session,
+ final ContentBlocking.BlockEvent event) {
+ Log.d(LOGTAG, "onContentBlocked" +
+ " AT: " + event.getAntiTrackingCategory() +
+ " SB: " + event.getSafeBrowsingCategory() +
+ " CB: " + event.getCookieBehaviorCategory() +
+ " URI: " + event.uri);
+ if ((event.getAntiTrackingCategory() &
+ ContentBlocking.AntiTracking.TEST) != 0) {
+ mBlockedTest++;
+ }
+ if ((event.getAntiTrackingCategory() &
+ ContentBlocking.AntiTracking.AD) != 0) {
+ mBlockedAds++;
+ }
+ if ((event.getAntiTrackingCategory() &
+ ContentBlocking.AntiTracking.ANALYTIC) != 0) {
+ mBlockedAnalytics++;
+ }
+ if ((event.getAntiTrackingCategory() &
+ ContentBlocking.AntiTracking.SOCIAL) != 0) {
+ mBlockedSocial++;
+ }
+ if ((event.getAntiTrackingCategory() &
+ ContentBlocking.AntiTracking.CONTENT) != 0) {
+ mBlockedContent++;
+ }
+ if ((event.getAntiTrackingCategory() &
+ ContentBlocking.AntiTracking.STP) != 0) {
+ mBlockedStp++;
+ }
+ }
+
+ @Override
+ public void onContentLoaded(final GeckoSession session,
+ final ContentBlocking.BlockEvent event) {
+ Log.d(LOGTAG, "onContentLoaded" +
+ " AT: " + event.getAntiTrackingCategory() +
+ " SB: " + event.getSafeBrowsingCategory() +
+ " CB: " + event.getCookieBehaviorCategory() +
+ " URI: " + event.uri);
+ }
+ }
+
+ private class ExampleMediaDelegate
+ implements GeckoSession.MediaDelegate {
+ private Integer mLastNotificationId = 100;
+ private Integer mNotificationId;
+ final private Activity mActivity;
+
+ public ExampleMediaDelegate(Activity activity) {
+ mActivity = activity;
+ }
+
+ @Override
+ public void onRecordingStatusChanged(@NonNull GeckoSession session, RecordingDevice[] devices) {
+ String message;
+ int icon;
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mActivity);
+ RecordingDevice camera = null;
+ RecordingDevice microphone = null;
+
+ for (RecordingDevice device : devices) {
+ if (device.type == RecordingDevice.Type.CAMERA) {
+ camera = device;
+ } else if (device.type == RecordingDevice.Type.MICROPHONE) {
+ microphone = device;
+ }
+ }
+ if (camera != null && microphone != null) {
+ Log.d(LOGTAG, "ExampleDeviceDelegate:onRecordingDeviceEvent display alert_mic_camera");
+ message = getResources().getString(R.string.device_sharing_camera_and_mic);
+ icon = R.drawable.alert_mic_camera;
+ } else if (camera != null) {
+ Log.d(LOGTAG, "ExampleDeviceDelegate:onRecordingDeviceEvent display alert_camera");
+ message = getResources().getString(R.string.device_sharing_camera);
+ icon = R.drawable.alert_camera;
+ } else if (microphone != null){
+ Log.d(LOGTAG, "ExampleDeviceDelegate:onRecordingDeviceEvent display alert_mic");
+ message = getResources().getString(R.string.device_sharing_microphone);
+ icon = R.drawable.alert_mic;
+ } else {
+ Log.d(LOGTAG, "ExampleDeviceDelegate:onRecordingDeviceEvent dismiss any notifications");
+ if (mNotificationId != null) {
+ notificationManager.cancel(mNotificationId);
+ mNotificationId = null;
+ }
+ return;
+ }
+ if (mNotificationId == null) {
+ mNotificationId = ++mLastNotificationId;
+ }
+
+ Intent intent = new Intent(mActivity, GeckoViewActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mActivity.getApplicationContext(), 0, intent, 0);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(mActivity.getApplicationContext(), CHANNEL_ID)
+ .setSmallIcon(icon)
+ .setContentTitle(getResources().getString(R.string.app_name))
+ .setContentText(message)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentIntent(pendingIntent)
+ .setCategory(NotificationCompat.CATEGORY_SERVICE);
+
+ notificationManager.notify(mNotificationId, builder.build());
+ }
+ }
+
+ private final class ExampleTelemetryDelegate
+ implements RuntimeTelemetry.Delegate {
+ @Override
+ public void onHistogram(final @NonNull RuntimeTelemetry.Histogram histogram) {
+ Log.d(LOGTAG, "onHistogram " + histogram);
+ }
+ @Override
+ public void onBooleanScalar(final @NonNull RuntimeTelemetry.Metric<Boolean> scalar) {
+ Log.d(LOGTAG, "onBooleanScalar " + scalar);
+ }
+ @Override
+ public void onLongScalar(final @NonNull RuntimeTelemetry.Metric<Long> scalar) {
+ Log.d(LOGTAG, "onLongScalar " + scalar);
+ }
+ @Override
+ public void onStringScalar(final @NonNull RuntimeTelemetry.Metric<String> scalar) {
+ Log.d(LOGTAG, "onStringScalar " + scalar);
+ }
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewBottomBehavior.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewBottomBehavior.java
new file mode 100644
index 0000000000..59edb2986e
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewBottomBehavior.java
@@ -0,0 +1,27 @@
+package org.mozilla.geckoview_example;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import androidx.appcompat.widget.Toolbar;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import org.mozilla.geckoview.GeckoView;
+
+public class GeckoViewBottomBehavior extends CoordinatorLayout.Behavior<GeckoView> {
+ public GeckoViewBottomBehavior(Context context, AttributeSet attributeSet) {
+ super(context, attributeSet);
+ }
+
+ @Override
+ public boolean layoutDependsOn(CoordinatorLayout parent, GeckoView child, View dependency) {
+ return dependency instanceof Toolbar;
+ }
+
+ @Override
+ public boolean onDependentViewChanged(CoordinatorLayout parent, GeckoView child, View dependency) {
+ child.setVerticalClipping(Math.round(-dependency.getTranslationY()));
+ return true;
+ }
+
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java
new file mode 100644
index 0000000000..076316643e
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java
@@ -0,0 +1,64 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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_example;
+
+import android.content.Context;
+import androidx.appcompat.widget.AppCompatEditText;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
+
+public class LocationView extends AppCompatEditText {
+
+ private CommitListener mCommitListener;
+ private FocusAndCommitListener mFocusCommitListener = new FocusAndCommitListener();
+
+ public interface CommitListener {
+ void onCommit(String text);
+ }
+
+ public LocationView(Context context) {
+ super(context);
+
+ this.setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_URI);
+ this.setSingleLine(true);
+ this.setSelectAllOnFocus(true);
+ this.setHint(R.string.location_hint);
+
+ setOnFocusChangeListener(mFocusCommitListener);
+ setOnEditorActionListener(mFocusCommitListener);
+ }
+
+ public void setCommitListener(CommitListener listener) {
+ mCommitListener = listener;
+ }
+
+ private class FocusAndCommitListener implements OnFocusChangeListener, OnEditorActionListener {
+ private String mInitialText;
+ private boolean mCommitted;
+
+ @Override
+ public void onFocusChange(View view, boolean focused) {
+ if (focused) {
+ mInitialText = ((TextView)view).getText().toString();
+ mCommitted = false;
+ } else if (!mCommitted) {
+ setText(mInitialText);
+ }
+ }
+
+ @Override
+ public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
+ if (mCommitListener != null) {
+ mCommitListener.onCommit(textView.getText().toString());
+ }
+
+ mCommitted = true;
+ return true;
+ }
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/NestedGeckoView.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/NestedGeckoView.java
new file mode 100644
index 0000000000..6afe371e3f
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/NestedGeckoView.java
@@ -0,0 +1,174 @@
+/* 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_example;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import androidx.core.view.NestedScrollingChild;
+import androidx.core.view.NestedScrollingChildHelper;
+import androidx.core.view.ViewCompat;
+import org.mozilla.geckoview.GeckoResult;
+import org.mozilla.geckoview.GeckoView;
+import org.mozilla.geckoview.PanZoomController;
+
+
+/**
+ * GeckoView that supports nested scrolls (for using in a CoordinatorLayout).
+ *
+ * This code is a simplified version of the NestedScrollView implementation
+ * which can be found in the support library:
+ * [android.support.v4.widget.NestedScrollView]
+ *
+ * Based on:
+ * https://github.com/takahirom/webview-in-coordinatorlayout
+ */
+
+public class NestedGeckoView extends GeckoView implements NestedScrollingChild {
+
+ private int mLastY;
+ private final int[] mScrollOffset = new int[2];
+ private final int[] mScrollConsumed = new int[2];
+ private int mNestedOffsetY;
+ private NestedScrollingChildHelper mChildHelper;
+
+ /**
+ * Integer indicating how user's MotionEvent was handled.
+ *
+ * There must be a 1-1 relation between this values and [EngineView.InputResult]'s.
+ */
+ private int mInputResult = PanZoomController.INPUT_RESULT_UNHANDLED;
+
+ public NestedGeckoView(final Context context) {
+ this(context, null);
+ }
+
+ public NestedGeckoView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mChildHelper = new NestedScrollingChildHelper(this);
+ setNestedScrollingEnabled(true);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ MotionEvent event = MotionEvent.obtain(ev);
+ final int action = event.getActionMasked();
+ int eventY = (int) event.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ final boolean allowScroll = !shouldPinOnScreen() &&
+ mInputResult == PanZoomController.INPUT_RESULT_HANDLED;
+ int deltaY = mLastY - eventY;
+
+ if (allowScroll && dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
+ deltaY -= mScrollConsumed[1];
+ event.offsetLocation(0f, -mScrollOffset[1]);
+ mNestedOffsetY += mScrollOffset[1];
+ }
+
+ mLastY = eventY - mScrollOffset[1];
+
+ if (allowScroll && dispatchNestedScroll(0, mScrollOffset[1], 0, deltaY, mScrollOffset)) {
+ mLastY -= mScrollOffset[1];
+ event.offsetLocation(0f, mScrollOffset[1]);
+ mNestedOffsetY += mScrollOffset[1];
+ }
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ // A new gesture started. Reset handled status and ask GV if it can handle this.
+ mInputResult = PanZoomController.INPUT_RESULT_UNHANDLED;
+ updateInputResult(event);
+
+ mNestedOffsetY = 0;
+ mLastY = eventY;
+
+ // The event should be handled either by onTouchEvent,
+ // either by onTouchEventForResult, never by both.
+ // Early return if we sent it to updateInputResult(..) which calls onTouchEventForResult.
+ event.recycle();
+ return true;
+
+ // We don't care about other touch events
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ stopNestedScroll();
+ break;
+ }
+
+ // Execute event handler from parent class in all cases
+ final boolean eventHandled = callSuperOnTouchEvent(event);
+
+ // Recycle previously obtained event
+ event.recycle();
+
+ return eventHandled;
+ }
+
+ private boolean callSuperOnTouchEvent(MotionEvent event) {
+ return super.onTouchEvent(event);
+ }
+
+ private void updateInputResult(MotionEvent event) {
+ super.onTouchEventForResult(event).accept(inputResult -> {
+ mInputResult = inputResult;
+ startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
+ });
+ }
+
+ public int getInputResult() {
+ return mInputResult;
+ }
+
+ @Override
+ public void setNestedScrollingEnabled(boolean enabled) {
+ mChildHelper.setNestedScrollingEnabled(enabled);
+ }
+
+ @Override
+ public boolean isNestedScrollingEnabled() {
+ return mChildHelper.isNestedScrollingEnabled();
+ }
+
+ @Override
+ public boolean startNestedScroll(int axes) {
+ return mChildHelper.startNestedScroll(axes);
+ }
+
+ @Override
+ public void stopNestedScroll() {
+ mChildHelper.stopNestedScroll();
+ }
+
+ @Override
+ public boolean hasNestedScrollingParent() {
+ return mChildHelper.hasNestedScrollingParent();
+ }
+
+ @Override
+ public boolean dispatchNestedScroll(int dxConsumed,
+ int dyConsumed,
+ int dxUnconsumed,
+ int dyUnconsumed,
+ int[] offsetInWindow) {
+ return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
+ }
+
+ @Override
+ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
+ return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
+ }
+
+ @Override
+ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
+ return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
+ }
+
+ @Override
+ public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
+ return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SessionActivity.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SessionActivity.java
new file mode 100644
index 0000000000..c6906bb1c3
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SessionActivity.java
@@ -0,0 +1,4 @@
+package org.mozilla.geckoview_example;
+
+public class SessionActivity extends GeckoViewActivity {
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SettingsActivity.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SettingsActivity.java
new file mode 100644
index 0000000000..127bc61fcc
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SettingsActivity.java
@@ -0,0 +1,40 @@
+package org.mozilla.geckoview_example;
+
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.appcompat.widget.Toolbar;
+
+public class SettingsActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_settings);
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.container, new SettingsFragment())
+ .commit();
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ // add back arrow to toolbar
+ if (getSupportActionBar() != null){
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+ }
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ onBackPressed();
+ return true;
+ }
+
+ public static class SettingsFragment extends PreferenceFragmentCompat {
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferencesFromResource(R.xml.settings, rootKey);
+ }
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java
new file mode 100644
index 0000000000..bfe3a66c04
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java
@@ -0,0 +1,43 @@
+package org.mozilla.geckoview_example;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+
+import org.mozilla.geckoview.GeckoSession;
+import org.mozilla.geckoview.GeckoSessionSettings;
+import org.mozilla.geckoview.WebExtension;
+
+public class TabSession extends GeckoSession {
+ private String mTitle;
+ private String mUri;
+ public WebExtension.Action action;
+
+ public TabSession() { super(); }
+
+ public TabSession(GeckoSessionSettings settings) {
+ super(settings);
+ }
+
+ public String getTitle() {
+ return mTitle == null || mTitle.length() == 0 ? "about:blank" : mTitle;
+ }
+
+ public void setTitle(String title) {
+ this.mTitle = title;
+ }
+
+ public String getUri() {
+ return mUri;
+ }
+
+ @Override
+ public void loadUri(@NonNull String uri) {
+ super.loadUri(uri);
+ mUri = uri;
+ }
+
+ public void onLocationChange(@NonNull String uri) {
+ mUri = uri;
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java
new file mode 100644
index 0000000000..9533190a5d
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java
@@ -0,0 +1,119 @@
+package org.mozilla.geckoview_example;
+
+import androidx.annotation.Nullable;
+
+import org.mozilla.geckoview.GeckoSession;
+import org.mozilla.geckoview.GeckoSessionSettings;
+import org.mozilla.geckoview.WebExtension;
+
+import java.util.ArrayList;
+
+public class TabSessionManager {
+ private static ArrayList<TabSession> mTabSessions = new ArrayList<>();
+ private int mCurrentSessionIndex = 0;
+ private TabObserver mTabObserver;
+ private boolean mTrackingProtection;
+
+ public interface TabObserver {
+ void onCurrentSession(TabSession session);
+ }
+
+ public TabSessionManager() {
+ }
+
+ public void unregisterWebExtension() {
+ for (final TabSession session : mTabSessions) {
+ session.action = null;
+ }
+ }
+
+ public void setWebExtensionDelegates(WebExtension extension,
+ WebExtension.ActionDelegate actionDelegate,
+ WebExtension.SessionTabDelegate tabDelegate) {
+ for (final TabSession session : mTabSessions) {
+ final WebExtension.SessionController sessionController =
+ session.getWebExtensionController();
+ sessionController.setActionDelegate(extension, actionDelegate);
+ sessionController.setTabDelegate(extension, tabDelegate);
+ }
+ }
+
+ public void setUseTrackingProtection(boolean trackingProtection) {
+ if (trackingProtection == mTrackingProtection) {
+ return;
+ }
+ mTrackingProtection = trackingProtection;
+
+ for (final TabSession session : mTabSessions) {
+ session.getSettings().setUseTrackingProtection(trackingProtection);
+ }
+ }
+
+ public void setTabObserver(TabObserver observer) {
+ mTabObserver = observer;
+ }
+
+ public void addSession(TabSession session) {
+ mTabSessions.add(session);
+ }
+
+ public TabSession getSession(int index) {
+ if (index >= mTabSessions.size()) {
+ return null;
+ }
+ return mTabSessions.get(index);
+ }
+
+ public TabSession getCurrentSession() {
+ return getSession(mCurrentSessionIndex);
+ }
+
+ public TabSession getSession(GeckoSession session) {
+ int index = mTabSessions.indexOf(session);
+ if (index == -1) {
+ return null;
+ }
+ return getSession(index);
+ }
+
+ public void setCurrentSession(TabSession session) {
+ int index = mTabSessions.indexOf(session);
+ if (index == -1) {
+ mTabSessions.add(session);
+ index = mTabSessions.size() - 1;
+ }
+ mCurrentSessionIndex = index;
+
+ if (mTabObserver != null) {
+ mTabObserver.onCurrentSession(session);
+ }
+ }
+
+ private boolean isCurrentSession(TabSession session) {
+ return session == getCurrentSession();
+ }
+
+ public void closeSession(@Nullable TabSession session) {
+ if (session == null) { return; }
+ if (isCurrentSession(session)
+ && mCurrentSessionIndex == mTabSessions.size() - 1) {
+ --mCurrentSessionIndex;
+ }
+ session.close();
+ mTabSessions.remove(session);
+ }
+
+ public TabSession newSession(GeckoSessionSettings settings) {
+ TabSession tabSession = new TabSession(settings);
+ mTabSessions.add(tabSession);
+ return tabSession;
+ }
+
+ public int sessionCount() {
+ return mTabSessions.size();
+ }
+
+ public ArrayList<TabSession> getSessions() {
+ return mTabSessions;
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarBottomBehavior.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarBottomBehavior.java
new file mode 100644
index 0000000000..93fd4e1eaf
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarBottomBehavior.java
@@ -0,0 +1,64 @@
+package org.mozilla.geckoview_example;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.MotionEvent;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.view.ViewCompat;
+import org.mozilla.geckoview.GeckoView;
+import org.mozilla.geckoview.PanZoomController;
+
+public class ToolbarBottomBehavior extends CoordinatorLayout.Behavior<View> {
+ public ToolbarBottomBehavior(Context context, AttributeSet attributeSet) {
+ super(context, attributeSet);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
+ View child,
+ View directTargetChild,
+ View target,
+ int axes,
+ int type) {
+ NestedGeckoView geckoView = (NestedGeckoView)target;
+ if (axes == ViewCompat.SCROLL_AXIS_VERTICAL &&
+ geckoView.getInputResult() == PanZoomController.INPUT_RESULT_HANDLED) {
+ return true;
+ }
+
+ if (geckoView.getInputResult() == PanZoomController.INPUT_RESULT_UNHANDLED) {
+ // Restore the toolbar to the original (visible) state, this is what A-C does.
+ child.setTranslationY(0f);
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onStopNestedScroll(CoordinatorLayout coordinatorLayout,
+ View child,
+ View target,
+ int type) {
+ // Snap up or down the user stops scrolling.
+ if (child.getTranslationY() >= (child.getHeight() / 2f)) {
+ child.setTranslationY(child.getHeight());
+ } else {
+ child.setTranslationY(0f);
+ }
+ }
+
+ @Override
+ public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
+ View child,
+ View target,
+ int dx,
+ int dy,
+ int[] consumed,
+ int type) {
+ super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
+ child.setTranslationY(Math.max(0f, Math.min(child.getHeight(), child.getTranslationY() + dy)));
+ }
+
+}
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java
new file mode 100644
index 0000000000..15cb2355df
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java
@@ -0,0 +1,126 @@
+package org.mozilla.geckoview_example;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.GradientDrawable;
+
+import androidx.core.content.ContextCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+public class ToolbarLayout extends LinearLayout {
+ public interface TabListener {
+ void switchToTab(int tabId);
+ void onBrowserActionClick();
+ }
+
+ private LocationView mLocationView;
+ private Button mTabsCountButton;
+ private View mBrowserAction;
+ private TabListener mTabListener;
+ private TabSessionManager mSessionManager;
+
+ public ToolbarLayout(Context context, TabSessionManager sessionManager) {
+ super(context);
+ mSessionManager = sessionManager;
+ initView();
+ }
+
+ private void initView() {
+ setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f));
+ setOrientation(LinearLayout.HORIZONTAL);
+ mLocationView = new LocationView(getContext());
+ mLocationView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 1.0f));
+ mLocationView.setId(R.id.url_bar);
+ addView(mLocationView);
+
+ mTabsCountButton = getTabsCountButton();
+ addView(mTabsCountButton);
+
+ mBrowserAction = getBrowserAction();
+ addView(mBrowserAction);
+ }
+
+ private Button getTabsCountButton() {
+ Button button = new Button(getContext());
+ button.setLayoutParams(new LayoutParams(150, LayoutParams.MATCH_PARENT));
+ button.setId(R.id.tabs_button);
+ button.setOnClickListener(this::onTabButtonClicked);
+ button.setBackground(ContextCompat.getDrawable(getContext(), R.drawable.tab_number_background));
+ button.setTypeface(button.getTypeface(), Typeface.BOLD);
+ return button;
+ }
+
+ private View getBrowserAction() {
+ View browserAction = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE))
+ .inflate(R.layout.browser_action, this, false);
+ browserAction.setVisibility(GONE);
+ return browserAction;
+ }
+
+ public void setBrowserActionButton(ActionButton button) {
+ if (button == null) {
+ mBrowserAction.setVisibility(GONE);
+ return;
+ }
+
+ BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), button.icon);
+ ImageView view = mBrowserAction.findViewById(R.id.browser_action_icon);
+ view.setOnClickListener(this::onBrowserActionButtonClicked);
+ view.setBackground(drawable);
+
+ TextView badge = mBrowserAction.findViewById(R.id.browser_action_badge);
+ if (button.text != null && !button.text.equals("")) {
+ if (button.backgroundColor != null) {
+ GradientDrawable backgroundDrawable = ((GradientDrawable) badge.getBackground().mutate());
+ backgroundDrawable.setColor(button.backgroundColor);
+ backgroundDrawable.invalidateSelf();
+ }
+ if (button.textColor != null) {
+ badge.setTextColor(button.textColor);
+ }
+ badge.setText(button.text);
+ badge.setVisibility(VISIBLE);
+ } else {
+ badge.setVisibility(GONE);
+ }
+
+ mBrowserAction.setVisibility(VISIBLE);
+ }
+
+ public void onBrowserActionButtonClicked(View view) {
+ mTabListener.onBrowserActionClick();
+ }
+
+ public LocationView getLocationView() {
+ return mLocationView;
+ }
+
+ public void setTabListener(TabListener listener) {
+ this.mTabListener = listener;
+ }
+
+ public void updateTabCount() {
+ mTabsCountButton.setText(String.valueOf(mSessionManager.sessionCount()));
+ }
+
+ public void onTabButtonClicked(View view) {
+ PopupMenu tabButtonMenu = new PopupMenu(getContext(), mTabsCountButton);
+ for(int idx = 0; idx < mSessionManager.sessionCount(); ++idx) {
+ tabButtonMenu.getMenu().add(0, idx, idx,
+ mSessionManager.getSession(idx).getTitle());
+ }
+ tabButtonMenu.setOnMenuItemClickListener(item -> {
+ mTabListener.switchToTab(item.getItemId());
+ return true;
+ });
+ tabButtonMenu.show();
+ }
+
+}
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_camera.png b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_camera.png
new file mode 100644
index 0000000000..6c7b806fa9
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_camera.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic.png b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic.png
new file mode 100644
index 0000000000..f56254888b
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic_camera.png b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic_camera.png
new file mode 100644
index 0000000000..091ec077dd
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic_camera.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_crash.png b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_crash.png
new file mode 100644
index 0000000000..bb0a241e33
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_crash.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_status_logo.png b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_status_logo.png
new file mode 100644
index 0000000000..374ef69857
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_status_logo.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-mdpi/ic_crash.png b/mobile/android/geckoview_example/src/main/res/drawable-mdpi/ic_crash.png
new file mode 100644
index 0000000000..d385b3e5cc
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-mdpi/ic_crash.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_camera.png b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_camera.png
new file mode 100644
index 0000000000..efe4811071
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_camera.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic.png b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic.png
new file mode 100644
index 0000000000..198a7ba318
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic_camera.png b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic_camera.png
new file mode 100644
index 0000000000..26ba6520b9
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic_camera.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/ic_crash.png b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/ic_crash.png
new file mode 100644
index 0000000000..3d9bd68082
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/ic_crash.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_camera.png b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_camera.png
new file mode 100644
index 0000000000..5c21f5bd4f
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_camera.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic.png b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic.png
new file mode 100644
index 0000000000..561816f087
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic_camera.png b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic_camera.png
new file mode 100644
index 0000000000..3b34d229ed
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic_camera.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/ic_crash.png b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/ic_crash.png
new file mode 100644
index 0000000000..39d5c7f57c
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/ic_crash.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/logo.png b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/logo.png
new file mode 100644
index 0000000000..162891e993
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/logo.png
Binary files differ
diff --git a/mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml b/mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml
new file mode 100644
index 0000000000..9cfaf557e2
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid
+ android:id="@+id/browser_action_badge_background"
+ android:color="#176d7a"
+ />
+ <corners android:radius="5dp" />
+</shape> \ No newline at end of file
diff --git a/mobile/android/geckoview_example/src/main/res/drawable/tab_number_background.xml b/mobile/android/geckoview_example/src/main/res/drawable/tab_number_background.xml
new file mode 100644
index 0000000000..d5292fda8f
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable/tab_number_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <solid android:color="@android:color/transparent" />
+
+ <corners
+ android:bottomRightRadius="2dp"
+ android:bottomLeftRadius="2dp"
+ android:topLeftRadius="2dp"
+ android:topRightRadius="2dp"/>
+
+ <stroke
+ android:width="1dp"
+ android:color="@android:color/darker_gray" />
+
+</shape> \ No newline at end of file
diff --git a/mobile/android/geckoview_example/src/main/res/layout/activity_settings.xml b/mobile/android/geckoview_example/src/main/res/layout/activity_settings.xml
new file mode 100644
index 0000000000..d6c4025ea6
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/colorBackgroundDark"
+ app:theme="@style/ToolBarStyle"
+ android:elevation="4dp"/>
+
+ <FrameLayout
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="@color/colorPrimaryDark"/>
+
+</LinearLayout>
diff --git a/mobile/android/geckoview_example/src/main/res/layout/browser_action.xml b/mobile/android/geckoview_example/src/main/res/layout/browser_action.xml
new file mode 100644
index 0000000000..fa06e95e88
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/browser_action.xml
@@ -0,0 +1,32 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="?android:actionBarSize"
+ android:layout_height="?android:actionBarSize"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+ <ImageView
+ android:id="@+id/browser_action_icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_centerInParent="true"
+ />
+ </RelativeLayout>
+
+ <TextView
+ android:id="@+id/browser_action_badge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/rounded_bg"
+ android:textColor="@color/colorPrimaryDark"
+ android:layout_alignParentRight="true"
+ android:paddingLeft="3dp"
+ android:paddingRight="3dp"
+ android:layout_marginTop="3dp"
+ android:layout_marginRight="3dp"
+ android:text="12"
+ />
+</RelativeLayout>
diff --git a/mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml b/mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml
new file mode 100644
index 0000000000..a1bb627d17
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml
@@ -0,0 +1,13 @@
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <org.mozilla.geckoview.GeckoView
+ android:id="@+id/gecko_view_popup"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="none"
+ />
+</RelativeLayout>
diff --git a/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
new file mode 100644
index 0000000000..8ee4a615a9
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
@@ -0,0 +1,32 @@
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.mozilla.geckoview_example.NestedGeckoView
+ android:id="@+id/gecko_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="none"
+ app:layout_behavior="org.mozilla.geckoview_example.GeckoViewBottomBehavior"
+ />
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?android:actionBarSize"
+ android:layout_gravity="bottom"
+ android:background="#eeeeee"
+ app:layout_behavior="org.mozilla.geckoview_example.ToolbarBottomBehavior"
+ app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed" />
+
+ <ProgressBar
+ android:id="@+id/page_progress"
+ style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="3dp"
+ android:layout_alignTop="@id/gecko_view" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/mobile/android/geckoview_example/src/main/res/menu/actions.xml b/mobile/android/geckoview_example/src/main/res/menu/actions.xml
new file mode 100644
index 0000000000..14afe9495b
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/menu/actions.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:title="@string/tracking_protection_ex" android:checkable="true"
+ android:id="@+id/action_tpe" app:showAsAction="never" />
+ <item android:title="@string/desktop_mode" android:id="@+id/desktop_mode" android:checkable="true"
+ app:showAsAction="never" />
+ <item android:title="@string/private_browsing" android:checkable="true" android:id="@+id/action_pb"/>
+ <item android:title="@string/new_tab" android:id="@+id/action_new_tab"/>
+ <item android:title="@string/install_addon" android:id="@+id/install_addon"/>
+ <item android:title="@string/update_addon" android:id="@+id/update_addon"/>
+ <item android:title="@string/close_tab" android:id="@+id/action_close_tab"/>
+ <item android:title="@string/forward" android:id="@+id/action_forward"/>
+ <item android:title="@string/reload" android:id="@+id/action_reload"/>
+ <item android:title="@string/settings" android:id="@+id/settings" app:showAsAction="never" />
+</menu>
diff --git a/mobile/android/geckoview_example/src/main/res/values/colors.xml b/mobile/android/geckoview_example/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..157834fe0c
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorBackgroundDark">#00a996</color>
+ <color name="colorPrimaryDark">#FFFFFF</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/mobile/android/geckoview_example/src/main/res/values/ids.xml b/mobile/android/geckoview_example/src/main/res/values/ids.xml
new file mode 100644
index 0000000000..068f6e3d95
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/ids.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="toolbar_layout" type="id"/>
+ <item name="url_bar" type="id"/>
+ <item name="browser_action" type="id"/>
+ <item name="tabs_button" type="id"/>
+</resources>
diff --git a/mobile/android/geckoview_example/src/main/res/values/strings.xml b/mobile/android/geckoview_example/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..744afc1a1c
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -0,0 +1,133 @@
+<resources>
+ <string name="app_name">GeckoView Example</string>
+ <string name="activity_label">GeckoView Example</string>
+ <string name="location_hint">Enter URL or search keywords...</string>
+ <string name="username">Username</string>
+ <string name="password">Password</string>
+ <string name="clear_field">Clear</string>
+ <string name="request_storage">Allow access to device storage for "%1$s"?</string>
+ <string name="request_geolocation">Share location with "%1$s"?</string>
+ <string name="request_notification">Allow notifications for "%1$s"?</string>
+ <string name="request_video">Share video with "%1$s"</string>
+ <string name="request_audio">Share audio with "%1$s"</string>
+ <string name="request_media">Share video and audio with "%1$s"</string>
+ <string name="request_xr">Share WebXR displays with "%1$s"?</string>
+ <string name="request_autoplay">Allow video to autoplay on "%1$s"?</string>
+ <string name="request_media_key_system_access">Allow system media key access for "%1$s"?</string>
+ <string name="media_back_camera">Back camera</string>
+ <string name="media_front_camera">Front camera</string>
+ <string name="media_microphone">Microphone</string>
+ <string name="media_other">Unknown source</string>
+
+ <string name="crash_native">Native</string>
+ <string name="crash_java">Java</string>
+ <string name="crash_content_native">Content (Native)</string>
+ <string name="tracking_protection">Tracking Protection</string>
+ <string name="tracking_protection_ex">TP exception</string>
+ <string name="private_browsing">Private Browsing</string>
+ <string name="remote_debugging">Remote Debugging</string>
+ <string name="forward">Forward</string>
+ <string name="reload">Reload</string>
+ <string name="settings">Settings...</string>
+ <string name="crashed_title">GeckoView Example Crashed</string>
+ <string name="crashed_text">Tap to report to Mozilla.</string>
+ <string name="crashed_ignore">Ignore</string>
+ <string name="device_sharing_microphone">Microphone is on</string>
+ <string name="device_sharing_camera">Camera is on</string>
+ <string name="device_sharing_camera_and_mic">Camera and microphone are on</string>
+ <string name="new_tab">New tab</string>
+ <string name="install_addon">Install Add-on...</string>
+ <string name="install_addon_hint">Add-on URL: https://addons.mozilla.org/...</string>
+ <string name="update_addon">Update Add-on</string>
+ <string name="close_tab">Close tab</string>
+ <string name="desktop_mode">Desktop site</string>
+ <string name="slow_script">A script on this page is causing your web browser to run slowly</string>
+ <string name="wait">Wait</string>
+ <string name="stop">Stop</string>
+ <string name="install">Install</string>
+ <string name="cancel">Cancel</string>
+ <string name="addon_uri">WebExtension xpi URL</string>
+
+ # Preferences
+ <string name="key_tracking_protection">tracking_protection</string>
+ <item type="bool" name="tracking_protection_default">true</item>
+
+ <string name="key_javascript_enabled">javascript_enabled</string>
+ <item type="bool" name="javascript_enabled_default">true</item>
+
+ <string name="key_remote_debugging">remote_debugging</string>
+ <item type="bool" name="remote_debugging_default">true</item>
+
+ <string name="key_dfpi">dfpi</string>
+ <item type="bool" name="dfpi_default">false</item>
+
+ <string name="key_autoplay">autoplay</string>
+ <item type="bool" name="autoplay_default">false</item>
+
+ <string name="key_allow_extensions_in_private_browsing">allow_extensions_in_private_browsing</string>
+ <item type="bool" name="allow_extensions_in_private_browsing_default">false</item>
+
+ <string name="key_user_agent_override">user_agent_override</string>
+ <string name="user_agent_override_default"></string>
+ <string-array name="user_agent_override_display_names">
+ <item>Default</item>
+ <item>Chrome 80 on Android</item>
+ <item>Safari 12 on iPhone</item>
+ </string-array>
+ <string-array name="user_agent_override_values">
+ <item></item>
+ <item>Mozilla/5.0 (Linux; Android 10; Z832 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Mobile Safari/537.36</item>
+ <item>Mozilla/5.0 (iPhone; CPU OS 10_15_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/14E304 Safari/605.1.15</item>
+ </string-array>
+
+ <string name="key_enhanced_tracking_protection">enhanced_tracking_protection</string>
+ <string name="enhanced_tracking_protection_default">standard</string>
+ <string-array name="enhanced_tracking_protection_display_names">
+ <item>Disabled</item>
+ <item>Enabled (standard)</item>
+ <item>Enabled (strict)</item>
+ </string-array>
+ <string-array name="enhanced_tracking_protection_values">
+ <item>disabled</item>
+ <item>standard</item>
+ <item>strict</item>
+ </string-array>
+
+ <string name="key_preferred_color_scheme">preferred_color_scheme</string>
+ <item type="integer" name="preferred_color_scheme_default">-1</item>
+ <string-array name="pref_preferred_color_scheme_display_names">
+ <item>Follow System Preference</item>
+ <item>Light</item>
+ <item>Dark</item>
+ </string-array>
+ <string-array name="pref_preferred_color_scheme_values">
+ <item>-1</item>
+ <item>0</item>
+ <item>1</item>
+ </string-array>
+
+ <string name="key_display_mode">display_mode</string>
+ <item type="integer" name="display_mode_default">0</item>
+ <string-array name="pref_display_mode_names">
+ <item>Browser</item>
+ <item>MinimalUi</item>
+ <item>Standalone</item>
+ <item>Fullscreen</item>
+ </string-array>
+ <string-array name="pref_display_mode_values">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </string-array>
+
+ <string name="before_unload_message">This page is asking you to confirm that you want to leave - data you have entered may not be saved</string>
+ <string name="before_unload_title">Are you sure?</string>
+ <string name="before_unload_leave_page">Leave Page</string>
+ <string name="before_unload_stay">Stay on Page</string>
+
+ <string name="repost_confirm_message">To display this page, GeckoViewExample must send information that will repeat any action (such as a search or order confirmation) that was performed earlier.</string>
+ <string name="repost_confirm_title">Are you sure?</string>
+ <string name="repost_confirm_resend">Resend</string>
+ <string name="repost_confirm_cancel">Cancel</string>
+</resources>
diff --git a/mobile/android/geckoview_example/src/main/res/values/styles.xml b/mobile/android/geckoview_example/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..e8420145ba
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/styles.xml
@@ -0,0 +1,7 @@
+<resources>
+ <style name="ToolBarStyle" parent="Theme.AppCompat">
+ <item name="android:textColorPrimary">@android:color/white</item>
+ <item name="android:textColorSecondary">@android:color/white</item>
+ <item name="actionMenuTextColor">@android:color/white</item>
+ </style>
+</resources>
diff --git a/mobile/android/geckoview_example/src/main/res/xml/settings.xml b/mobile/android/geckoview_example/src/main/res/xml/settings.xml
new file mode 100644
index 0000000000..34a6a0c0ab
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/xml/settings.xml
@@ -0,0 +1,57 @@
+<PreferenceScreen
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <SwitchPreferenceCompat
+ app:key="@string/key_tracking_protection"
+ app:title="Enable Tracking Protection"
+ app:defaultValue="@bool/tracking_protection_default"/>
+ <ListPreference
+ app:key="@string/key_enhanced_tracking_protection"
+ app:title="Enhanced Tracking Protection"
+ app:summary="%s"
+ app:entries="@array/enhanced_tracking_protection_display_names"
+ app:entryValues="@array/enhanced_tracking_protection_values"
+ app:defaultValue="@string/enhanced_tracking_protection_default"/>
+ <SwitchPreferenceCompat
+ app:key="@string/key_dfpi"
+ app:title="Enable Dynamic FPI"
+ app:defaultValue="@bool/dfpi_default"/>
+ <SwitchPreferenceCompat
+ app:key="@string/key_autoplay"
+ app:title="Allow Autoplay"
+ app:defaultValue="@bool/autoplay_default"/>
+ <SwitchPreferenceCompat
+ app:key="@string/key_remote_debugging"
+ app:title="Remote Debugging"
+ app:defaultValue="@bool/remote_debugging_default"/>
+ <ListPreference
+ app:key="@string/key_preferred_color_scheme"
+ app:title="Preferred Color Scheme"
+ app:summary="%s"
+ app:entries="@array/pref_preferred_color_scheme_display_names"
+ app:entryValues="@array/pref_preferred_color_scheme_values"
+ app:defaultValue="@integer/preferred_color_scheme_default"/>
+ <ListPreference
+ app:key="@string/key_user_agent_override"
+ app:title="User Agent String Override"
+ app:summary="%s"
+ app:entries="@array/user_agent_override_display_names"
+ app:entryValues="@array/user_agent_override_values"
+ app:defaultValue="@string/user_agent_override_default"/>
+ <SwitchPreferenceCompat
+ app:key="@string/key_allow_extensions_in_private_browsing"
+ app:title="Run extensions in private tabs"
+ app:defaultValue="@bool/allow_extensions_in_private_browsing_default"/>
+ <ListPreference
+ app:key="@string/key_display_mode"
+ app:title="Display Mode"
+ app:summary="%s"
+ app:entries="@array/pref_display_mode_names"
+ app:entryValues="@array/pref_display_mode_values"
+ app:defaultValue="@integer/display_mode_default"/>
+ <SwitchPreferenceCompat
+ app:key="@string/key_javascript_enabled"
+ app:title="Javascript Enabled"
+ app:defaultValue="@bool/javascript_enabled_default"/>
+
+</PreferenceScreen>