From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- mobile/android/geckoview_example/build.gradle | 59 + .../android/geckoview_example/proguard-rules.pro | 17 + .../geckoview_example/src/main/AndroidManifest.xml | 73 + .../geckoview_example/src/main/assets/error.html | 31 + .../mozilla/geckoview_example/ActionButton.java | 25 + .../geckoview_example/BasicGeckoViewPrompt.java | 1127 +++++++++ .../geckoview_example/ExampleCrashHandler.java | 137 ++ .../geckoview_example/GeckoViewActivity.java | 2539 ++++++++++++++++++++ .../geckoview_example/GeckoViewBottomBehavior.java | 30 + .../mozilla/geckoview_example/LocationView.java | 64 + .../mozilla/geckoview_example/NestedGeckoView.java | 169 ++ .../mozilla/geckoview_example/SessionActivity.java | 7 + .../geckoview_example/SettingsActivity.java | 44 + .../org/mozilla/geckoview_example/TabSession.java | 46 + .../geckoview_example/TabSessionManager.java | 121 + .../geckoview_example/ToolbarBottomBehavior.java | 64 + .../mozilla/geckoview_example/ToolbarLayout.java | 131 + .../src/main/res/drawable-hdpi/alert_camera.png | Bin 0 -> 267 bytes .../src/main/res/drawable-hdpi/alert_mic.png | Bin 0 -> 474 bytes .../main/res/drawable-hdpi/alert_mic_camera.png | Bin 0 -> 242 bytes .../src/main/res/drawable-hdpi/ic_crash.png | Bin 0 -> 303 bytes .../src/main/res/drawable-hdpi/ic_status_logo.png | Bin 0 -> 717 bytes .../src/main/res/drawable-mdpi/ic_crash.png | Bin 0 -> 226 bytes .../src/main/res/drawable-xhdpi/alert_camera.png | Bin 0 -> 319 bytes .../src/main/res/drawable-xhdpi/alert_mic.png | Bin 0 -> 575 bytes .../main/res/drawable-xhdpi/alert_mic_camera.png | Bin 0 -> 268 bytes .../src/main/res/drawable-xhdpi/ic_crash.png | Bin 0 -> 386 bytes .../src/main/res/drawable-xxhdpi/alert_camera.png | Bin 0 -> 428 bytes .../src/main/res/drawable-xxhdpi/alert_mic.png | Bin 0 -> 829 bytes .../main/res/drawable-xxhdpi/alert_mic_camera.png | Bin 0 -> 487 bytes .../src/main/res/drawable-xxhdpi/ic_crash.png | Bin 0 -> 572 bytes .../src/main/res/drawable-xxhdpi/logo.png | Bin 0 -> 3808 bytes .../src/main/res/drawable/rounded_bg.xml | 9 + .../main/res/drawable/tab_number_background.xml | 20 + .../src/main/res/layout/activity_settings.xml | 24 + .../src/main/res/layout/browser_action.xml | 32 + .../src/main/res/layout/browser_action_popup.xml | 13 + .../src/main/res/layout/geckoview_activity.xml | 32 + .../src/main/res/menu/actions.xml | 19 + .../src/main/res/values/colors.xml | 7 + .../geckoview_example/src/main/res/values/ids.xml | 7 + .../src/main/res/values/strings.xml | 152 ++ .../src/main/res/values/styles.xml | 7 + .../src/main/res/xml/settings.xml | 71 + 44 files changed, 5077 insertions(+) create mode 100644 mobile/android/geckoview_example/build.gradle create mode 100644 mobile/android/geckoview_example/proguard-rules.pro create mode 100644 mobile/android/geckoview_example/src/main/AndroidManifest.xml create mode 100644 mobile/android/geckoview_example/src/main/assets/error.html create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ExampleCrashHandler.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewBottomBehavior.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/NestedGeckoView.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SessionActivity.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SettingsActivity.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarBottomBehavior.java create mode 100644 mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_camera.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic_camera.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_crash.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_status_logo.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-mdpi/ic_crash.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_camera.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic_camera.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xhdpi/ic_crash.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_camera.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic_camera.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/ic_crash.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/logo.png create mode 100644 mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml create mode 100644 mobile/android/geckoview_example/src/main/res/drawable/tab_number_background.xml create mode 100644 mobile/android/geckoview_example/src/main/res/layout/activity_settings.xml create mode 100644 mobile/android/geckoview_example/src/main/res/layout/browser_action.xml create mode 100644 mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml create mode 100644 mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml create mode 100644 mobile/android/geckoview_example/src/main/res/menu/actions.xml create mode 100644 mobile/android/geckoview_example/src/main/res/values/colors.xml create mode 100644 mobile/android/geckoview_example/src/main/res/values/ids.xml create mode 100644 mobile/android/geckoview_example/src/main/res/values/strings.xml create mode 100644 mobile/android/geckoview_example/src/main/res/values/styles.xml create mode 100644 mobile/android/geckoview_example/src/main/res/xml/settings.xml (limited to 'mobile/android/geckoview_example') diff --git a/mobile/android/geckoview_example/build.gradle b/mobile/android/geckoview_example/build.gradle new file mode 100644 index 0000000000..e84fc42b19 --- /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 project.ext.versionCode + versionName project.ext.versionName + + multiDexEnabled true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + 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.6.0" + implementation "androidx.appcompat:appcompat:1.6.1" + implementation "androidx.preference:preference:1.1.1" + + implementation project(path: ':geckoview') + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.google.android.material:material:1.9.0' + + implementation 'androidx.multidex:multidex:2.0.1' +} 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..280a1b6e77 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..4534e9d222 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/assets/error.html @@ -0,0 +1,31 @@ + + + + Boom! + + + + + +
+

Boom!

+

Something bad happened...

+

$ERROR

+
+ + 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..729fb6a61d --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java @@ -0,0 +1,25 @@ +/* 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.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..92dd677c3e --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java @@ -0,0 +1,1127 @@ +/* -*- 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 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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.Autocomplete; +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 mFileResponse; + private FilePrompt mFilePrompt; + + public BasicGeckoViewPrompt(final Activity activity) { + mActivity = activity; + } + + @Override + public GeckoResult 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 res = new GeckoResult(); + createStandardDialog(builder, prompt, res).show(); + return res; + } + + @Override + public GeckoResult 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 res = new GeckoResult(); + + 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 onSharePrompt( + final GeckoSession session, final SharePrompt prompt) { + return GeckoResult.fromValue(prompt.dismiss()); + } + + @Nullable + @Override + public GeckoResult 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 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 onCreditCardSave( + @NonNull GeckoSession session, + @NonNull AutocompleteRequest request) { + Log.i(LOGTAG, "onCreditCardSave " + request.options[0].value); + return null; + } + + @Nullable + @Override + public GeckoResult onLoginSave( + @NonNull GeckoSession session, + @NonNull AutocompleteRequest request) { + Log.i(LOGTAG, "onLoginSave"); + return GeckoResult.fromValue(request.confirm(request.options[0])); + } + + @Nullable + @Override + public GeckoResult 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 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 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 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 res = new GeckoResult(); + + 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 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 res = new GeckoResult(); + + 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 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 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 adapter = + new ArrayAdapter( + 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 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(); + + prompt.setDelegate( + new PromptInstanceDelegate() { + @Override + public void onPromptDismiss(final BasePrompt prompt) { + dialog.dismiss(); + } + + @Override + public void onPromptUpdate(final BasePrompt prompt) { + dialog.setOnDismissListener(null); + dialog.dismiss(); + final ChoicePrompt newPrompt = (ChoicePrompt) prompt; + onChoicePromptImpl( + session, + newPrompt.title, + newPrompt.message, + newPrompt.type, + newPrompt.choices, + newPrompt, + res); + } + }); + } + + @Override + public GeckoResult onChoicePrompt( + final GeckoSession session, final ChoicePrompt prompt) { + final GeckoResult res = new GeckoResult(); + 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 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 adapter = + new ArrayAdapter(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 res = new GeckoResult(); + + 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 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 res = new GeckoResult(); + + 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); + + final AlertDialog dialog = createStandardDialog(builder, prompt, res); + dialog.show(); + + prompt.setDelegate( + new PromptInstanceDelegate() { + @Override + public void onPromptDismiss(final BasePrompt prompt) { + dialog.dismiss(); + } + }); + return res; + } + + @Override + @TargetApi(19) + public GeckoResult 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 res = new GeckoResult(); + + 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 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 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 GeckoResult onPermissionPrompt( + final GeckoSession session, + final String title, + final GeckoSession.PermissionDelegate.ContentPermission perm) { + final Activity activity = mActivity; + final GeckoResult res = new GeckoResult<>(); + if (activity == null) { + res.complete(GeckoSession.PermissionDelegate.ContentPermission.VALUE_PROMPT); + return res; + } + 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) { + res.complete(GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY); + } + }) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + res.complete(GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW); + } + }); + + final AlertDialog dialog = builder.create(); + dialog.show(); + return res; + } + + public void onSlowScriptPrompt( + GeckoSession geckoSession, String title, GeckoResult 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 adapter = + new ArrayAdapter(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 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..1c66757483 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ExampleCrashHandler.java @@ -0,0 +1,137 @@ +/* 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.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 android.util.Log; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import org.mozilla.geckoview.BuildConfig; +import org.mozilla.geckoview.CrashReporter; +import org.mozilla.geckoview.GeckoRuntime; + +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, + "Process Type: " + mCrashIntent.getStringExtra(GeckoRuntime.EXTRA_CRASH_PROCESS_TYPE)); + + String id = createNotificationChannel(); + + int intentFlag = 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + intentFlag = PendingIntent.FLAG_IMMUTABLE; + } + + PendingIntent reportIntent = + PendingIntent.getService( + this, + 0, + new Intent(ACTION_REPORT_CRASH, null, this, ExampleCrashHandler.class), + intentFlag); + + PendingIntent dismissIntent = + PendingIntent.getService( + this, + 0, + new Intent(ACTION_DISMISS, null, this, ExampleCrashHandler.class), + intentFlag); + + 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) + .addAction(0, getResources().getString(R.string.crashed_ignore), dismissIntent) + .addAction(0, getResources().getString(R.string.crashed_report), reportIntent) + .setAutoCancel(true) + .setOngoing(false) + .build(); + + startForeground(NOTIFY_ID, notification); + } else if (ACTION_REPORT_CRASH.equals(intent.getAction())) { + StrictMode.ThreadPolicy oldPolicy = null; + if (BuildConfig.DEBUG_BUILD) { + 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..8731f9649c --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java @@ -0,0 +1,2539 @@ +/* -*- 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.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.SystemClock; +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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +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.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.json.JSONObject; +import org.mozilla.geckoview.AllowOrDeny; +import org.mozilla.geckoview.Autocomplete; +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.GeckoSession.PermissionDelegate; +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission; +import org.mozilla.geckoview.GeckoSessionSettings; +import org.mozilla.geckoview.GeckoView; +import org.mozilla.geckoview.GeckoWebExecutor; +import org.mozilla.geckoview.Image; +import org.mozilla.geckoview.MediaSession; +import org.mozilla.geckoview.OrientationController; +import org.mozilla.geckoview.RuntimeTelemetry; +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.WebResponse; + +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 mBitmapCache = new LruCache<>(5); + private GeckoRuntime mRuntime; + private WebExtension.Action mDefaultAction; + private TabSessionManager mTabManager; + + private WeakReference mExtensionDelegate; + + @Nullable + @Override + public GeckoResult onInstallPrompt(final @NonNull WebExtension extension) { + return GeckoResult.allow(); + } + + @Nullable + @Override + public GeckoResult onUpdatePrompt( + @NonNull WebExtension currentlyInstalled, + @NonNull WebExtension updatedExtension, + @NonNull String[] newPermissions, + @NonNull String[] newOrigins) { + return GeckoResult.allow(); + } + + @Nullable + @Override + public GeckoResult onOptionalPrompt( + final @NonNull WebExtension extension, final String[] permissions, final String[] origins) { + return GeckoResult.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 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 onCloseTab(WebExtension extension, GeckoSession session) { + final WebExtensionDelegate delegate = mExtensionDelegate.get(); + if (delegate == null) { + return GeckoResult.deny(); + } + + final TabSession tabSession = mTabManager.getSession(session); + if (tabSession != null) { + delegate.closeTab(tabSession); + } + + return GeckoResult.allow(); + } + + @Override + public GeckoResult onUpdateTab( + WebExtension extension, GeckoSession session, WebExtension.UpdateTabDetails updateDetails) { + final WebExtensionDelegate delegate = mExtensionDelegate.get(); + if (delegate == null) { + return GeckoResult.deny(); + } + + final TabSession tabSession = mTabManager.getSession(session); + if (tabSession != null) { + delegate.updateTab(tabSession, updateDetails); + } + + return GeckoResult.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 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 onTogglePopup( + final @NonNull WebExtension extension, final @NonNull WebExtension.Action action) { + return togglePopup(false); + } + + @Override + public GeckoResult 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 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 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 mCollapsed; + private boolean mKillProcessOnDestroy; + private boolean mDesktopMode; + + private TabSession mPopupSession; + private View mPopupView; + private ContentPermission mTrackingProtectionPermission; + + private boolean mShowNotificationsRejected; + private ArrayList mAcceptedPersistentStorage = new ArrayList(); + + private ToolbarLayout mToolbarView; + private String mCurrentUri; + private boolean mCanGoBack; + private boolean mCanGoForward; + private boolean mFullScreen; + + private HashMap mNotificationIDMap = new HashMap<>(); + private int mLastID = 100; + + private ProgressBar mProgressView; + + private LinkedList mPendingDownloads = new LinkedList<>(); + + private int mNextActivityResultCode = 10; + private HashMap> mPendingActivityResult = new HashMap<>(); + + private LocationView.CommitListener mCommitListener = + new LocationView.CommitListener() { + @Override + public void onCommit(String text) { + if (text.startsWith("data:") + || ((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> SETTINGS = new ArrayList<>(); + + private abstract class Setting { + 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 { + 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 { + 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 { + 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 StringSetting mCookieBannerHandling = + new StringSetting( + R.string.key_cookie_banner_handling, R.string.cookie_banner_handling_default) { + @Override + public void setValue(final GeckoRuntimeSettings settings, final String value) { + int cbMode; + switch (value) { + case "disabled": + cbMode = ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_DISABLED; + break; + case "reject_all": + cbMode = ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_REJECT; + break; + case "reject_accept_all": + cbMode = ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_REJECT_OR_ACCEPT; + break; + default: + throw new RuntimeException("Invalid Cookie Banner Handling mode: " + value); + } + settings.getContentBlocking().setCookieBannerMode(cbMode); + } + }; + + private final StringSetting mCookieBannerHandlingPrivateMode = + new StringSetting( + R.string.key_cookie_banner_handling_pb, R.string.cookie_banner_handling_pb_default) { + @Override + public void setValue(final GeckoRuntimeSettings settings, final String value) { + int cbPrivateMode; + switch (value) { + case "disabled": + cbPrivateMode = ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_DISABLED; + break; + case "reject_all": + cbPrivateMode = ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_REJECT; + break; + case "reject_accept_all": + cbPrivateMode = ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_REJECT_OR_ACCEPT; + break; + default: + throw new RuntimeException("Invalid Cookie Banner Handling private mode: " + value); + } + settings.getContentBlocking().setCookieBannerModePrivateBrowsing(cbPrivateMode); + } + }; + + 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); + + // We might have been started because the user clicked on a notification + WebNotification notification = getIntent().getParcelableExtra("onClick"); + if (notification != null) { + getIntent().removeExtra("onClick"); + notification.click(); + } + + Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() + " - application start"); + createNotificationChannel(); + setContentView(R.layout.geckoview_activity); + mGeckoView = findViewById(R.id.gecko_view); + mGeckoView.setActivityContextDelegate(new ExampleActivityDelegate()); + 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) + .cookieBehaviorPrivateMode(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); + sGeckoRuntime.setAutocompleteStorageDelegate(new ExampleAutocompleteStorageDelegate()); + sGeckoRuntime.getOrientationController().setDelegate(new ExampleOrientationDelegate()); + sGeckoRuntime.setServiceWorkerDelegate( + new GeckoRuntime.ServiceWorkerDelegate() { + @NonNull + @Override + public GeckoResult onOpenWindow(@NonNull String url) { + return mNavigationDelegate.onNewSession(null, url); + } + }); + + // `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); + PendingIntent dismissIntent = + PendingIntent.getActivity( + GeckoViewActivity.this, mLastID, clickIntent, PendingIntent.FLAG_IMMUTABLE); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(GeckoViewActivity.this, CHANNEL_ID) + .setContentTitle(notification.title) + .setContentText(notification.text) + .setSmallIcon(R.drawable.ic_status_logo) + .setContentIntent(dismissIntent) + .setAutoCancel(true); + + mNotificationIDMap.put(notification.tag, mLastID); + + if (notification.imageUrl != null && notification.imageUrl.length() > 0) { + final GeckoWebExecutor executor = new GeckoWebExecutor(sGeckoRuntime); + + GeckoResult response = + executor.fetch( + new WebRequest.Builder(notification.imageUrl) + .addHeader("Accept", "image") + .build()); + response.accept( + value -> { + Bitmap bitmap = BitmapFactory.decodeStream(value.body); + builder.setLargeIcon(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); + mNotificationIDMap.remove(notification.tag); + } + } + }); + } + + sGeckoRuntime.setDelegate( + () -> { + mKillProcessOnDestroy = true; + finish(); + }); + + sGeckoRuntime.setActivityDelegate( + pendingIntent -> { + final GeckoResult 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; + setViewVisibility(mPopupView, shouldShow); + + return shouldShow ? mPopupSession : null; + } + + private static void setViewVisibility(final View view, final boolean visible) { + if (view == null) { + return; + } + + ViewGroup.LayoutParams params = view.getLayoutParams(); + + if (visible) { + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + params.height = 0; + params.width = 0; + } + + view.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) { + setViewVisibility(mPopupView, 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 final GeckoSession.NavigationDelegate mNavigationDelegate = + new ExampleNavigationDelegate(); + + 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(mNavigationDelegate); + + 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.setMediaSessionDelegate(new ExampleMediaSessionDelegate(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 && mGeckoView.getSession() != 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; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.action_pb).setChecked(mUsePrivateBrowsing); + menu.findItem(R.id.collapse).setChecked(mCollapsed); + menu.findItem(R.id.desktop_mode).setChecked(mDesktopMode); + menu.findItem(R.id.action_tpe) + .setChecked( + mTrackingProtectionPermission != null + && mTrackingProtectionPermission.value == ContentPermission.VALUE_ALLOW); + menu.findItem(R.id.action_forward).setEnabled(mCanGoForward); + + final boolean hasSession = mTabSessionManager.getCurrentSession() != null; + menu.findItem(R.id.action_reload).setEnabled(hasSession); + menu.findItem(R.id.action_forward).setEnabled(hasSession); + menu.findItem(R.id.action_close_tab).setEnabled(hasSession); + menu.findItem(R.id.action_tpe).setEnabled(hasSession && mTrackingProtectionPermission != null); + menu.findItem(R.id.action_pb).setEnabled(hasSession); + menu.findItem(R.id.desktop_mode).setEnabled(hasSession); + + 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 + .getStorageController() + .setPermission( + mTrackingProtectionPermission, + mTrackingProtectionPermission.value == ContentPermission.VALUE_ALLOW + ? ContentPermission.VALUE_DENY + : ContentPermission.VALUE_ALLOW); + 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.collapse: + mCollapsed = !mCollapsed; + setViewVisibility(mGeckoView, !mCollapsed); + 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; + case R.id.save_pdf: + savePdf(session); + break; + case R.id.print_page: + printPage(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 + setViewVisibility(mPopupView, 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); + } + + @SuppressLint("WrongThread") + @UiThread + private void savePdf(GeckoSession session) { + session + .saveAsPdf() + .accept( + pdfStream -> { + try { + WebResponse response = + new WebResponse.Builder(null) + .body(pdfStream) + .addHeader("Content-Type", "application/pdf") + .addHeader("Content-Disposition", "attachment; filename=PDFDownload.pdf") + .build(); + session.getContentDelegate().onExternalResponse(session, response); + + } catch (Exception e) { + Log.d(LOGTAG, e.getMessage()); + } + }); + } + + private void printPage(GeckoSession session) { + session.printPageContent(); + } + + @Override + public void closeTab(TabSession session) { + mTabSessionManager.closeSession(session); + TabSession tabSession = mTabSessionManager.getCurrentSession(); + setGeckoViewSession(tabSession); + if (tabSession != null) { + tabSession.reload(); + } + mToolbarView.updateTabCount(); + } + + @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.getSession(); + if (previousSession != null) { + controller.setTabActive(previousSession, false); + } + + final boolean hasSession = session != null; + final LocationView view = mToolbarView.getLocationView(); + // No point having the URL bar enabled if there's no session to navigate to + view.setEnabled(hasSession); + + if (hasSession) { + mGeckoView.setSession(session); + if (activateTab) { + controller.setTabActive(session, true); + } + mTabSessionManager.setCurrentSession(session); + } else { + mGeckoView.coverUntilFirstPaint(Color.WHITE); + view.setText(""); + } + } + + @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")) { + WebNotification notification = intent.getExtras().getParcelable("onClick"); + if (notification != null) { + intent.removeExtra("onClick"); + notification.click(); + } + } + + 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 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 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; + + Log.i(LOGTAG, "Downloading to: " + downloadsPath); + 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 mVisitedURLs; + + private ExampleHistoryDelegate() { + mVisitedURLs = new HashSet(); + } + + @Override + public GeckoResult 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 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 ExampleAutocompleteStorageDelegate implements Autocomplete.StorageDelegate { + private Map mStorage = new HashMap<>(); + + @Nullable + @Override + public GeckoResult onLoginFetch() { + return GeckoResult.fromValue(mStorage.values().toArray(new Autocomplete.LoginEntry[0])); + } + + @Override + public void onLoginSave(@NonNull Autocomplete.LoginEntry login) { + mStorage.put(login.guid, login); + } + } + + private class ExampleOrientationDelegate implements OrientationController.OrientationDelegate { + @Override + public GeckoResult onOrientationLock(@NonNull int aOrientation) { + setRequestedOrientation(aOrientation); + return GeckoResult.allow(); + } + + @Override + public void onOrientationUnlock() { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + } + + 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) { + final TabSession currentSession = mTabSessionManager.getCurrentSession(); + if (session == currentSession) { + closeTab(currentSession); + } + } + + @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 onSlowScript( + final GeckoSession geckoSession, final String scriptFileName) { + BasicGeckoViewPrompt prompt = + (BasicGeckoViewPrompt) mTabSessionManager.getCurrentSession().getPromptDelegate(); + if (prompt != null) { + GeckoResult result = new GeckoResult(); + 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); + } + + @Override + public void onShowDynamicToolbar(final GeckoSession session) { + final View toolbar = findViewById(R.id.toolbar); + if (toolbar != null) { + toolbar.setTranslationY(0f); + mGeckoView.setVerticalClipping(0); + } + } + + @Override + public void onCookieBannerDetected(final GeckoSession session) { + Log.d("BELL", "A cookie banner was detected on this website"); + } + + @Override + public void onCookieBannerHandled(final GeckoSession session) { + Log.d("BELL", "A cookie banner was handled on this website"); + } + } + + 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 PermissionDelegate { + + public int androidPermissionRequestCode = 1; + private Callback mCallback; + + class ExampleNotificationCallback implements PermissionDelegate.Callback { + private final PermissionDelegate.Callback mCallback; + + ExampleNotificationCallback(final PermissionDelegate.Callback callback) { + mCallback = callback; + } + + @Override + public void reject() { + mShowNotificationsRejected = true; + mCallback.reject(); + } + + @Override + public void grant() { + mShowNotificationsRejected = false; + mCallback.grant(); + } + } + + class ExamplePersistentStorageCallback implements PermissionDelegate.Callback { + private final PermissionDelegate.Callback mCallback; + private final String mUri; + + ExamplePersistentStorageCallback(final 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 GeckoResult onContentPermissionRequest( + final GeckoSession session, final ContentPermission perm) { + final int resId; + switch (perm.permission) { + case PERMISSION_GEOLOCATION: + resId = R.string.request_geolocation; + break; + case PERMISSION_DESKTOP_NOTIFICATION: + resId = R.string.request_notification; + break; + case PERMISSION_PERSISTENT_STORAGE: + resId = R.string.request_storage; + break; + case PERMISSION_XR: + resId = R.string.request_xr; + break; + case PERMISSION_AUTOPLAY_AUDIBLE: + case PERMISSION_AUTOPLAY_INAUDIBLE: + if (!mAllowAutoplay.value()) { + return GeckoResult.fromValue(ContentPermission.VALUE_DENY); + } else { + return GeckoResult.fromValue(ContentPermission.VALUE_ALLOW); + } + case PERMISSION_MEDIA_KEY_SYSTEM_ACCESS: + resId = R.string.request_media_key_system_access; + break; + case PERMISSION_STORAGE_ACCESS: + resId = R.string.request_storage_access; + break; + default: + return GeckoResult.fromValue(ContentPermission.VALUE_DENY); + } + + final String title = getString(resId, Uri.parse(perm.uri).getAuthority()); + final BasicGeckoViewPrompt prompt = + (BasicGeckoViewPrompt) mTabSessionManager.getCurrentSession().getPromptDelegate(); + return prompt.onPermissionPrompt(session, title, perm); + } + + 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 ContentPermission getTrackingProtectionPermission(final List perms) { + for (ContentPermission perm : perms) { + if (perm.permission == PermissionDelegate.PERMISSION_TRACKING) { + return perm; + } + } + return null; + } + + private class ExampleNavigationDelegate implements GeckoSession.NavigationDelegate { + @Override + public void onLocationChange( + GeckoSession session, final String url, final List perms) { + mToolbarView.getLocationView().setText(url); + TabSession tabSession = mTabSessionManager.getSession(session); + if (tabSession != null) { + tabSession.onLocationChange(url); + } + mTrackingProtectionPermission = getTrackingProtectionPermission(perms); + mCurrentUri = url; + } + + @Override + public void onCanGoBack(GeckoSession session, boolean canGoBack) { + mCanGoBack = canGoBack; + } + + @Override + public void onCanGoForward(GeckoSession session, boolean canGoForward) { + mCanGoForward = canGoForward; + } + + @Override + public GeckoResult 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.allow(); + } + + @Override + public GeckoResult onSubframeLoadRequest( + final GeckoSession session, final LoadRequest request) { + Log.d( + LOGTAG, + "onSubframeLoadRequest=" + + request.uri + + " triggerUri=" + + request.triggerUri + + " isRedirect=" + + request.isRedirect + + "isDirectNavigation=" + + request.isDirectNavigation); + + return GeckoResult.allow(); + } + + @Override + public GeckoResult 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"; + case WebRequestError.ERROR_HTTPS_ONLY: + return "ERROR_HTTPS_ONLY"; + case WebRequestError.ERROR_BAD_HSTS_CERT: + return "ERROR_BAD_HSTS_CERT"; + 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 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; + private final 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, PendingIntent.FLAG_IMMUTABLE); + + 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 class ExampleMediaSessionDelegate implements MediaSession.Delegate { + private final Activity mActivity; + + public ExampleMediaSessionDelegate(Activity activity) { + mActivity = activity; + } + + @Override + public void onFullscreen( + @NonNull final GeckoSession session, + @NonNull final MediaSession mediaSession, + final boolean enabled, + @Nullable final MediaSession.ElementMetadata meta) { + Log.d(LOGTAG, "onFullscreen: Metadata=" + (meta != null ? meta.toString() : "null")); + + if (!enabled) { + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER); + return; + } + + if (meta == null) { + return; + } + + if (meta.width > meta.height) { + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + } else { + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + } + } + + 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 scalar) { + Log.d(LOGTAG, "onBooleanScalar " + scalar); + } + + @Override + public void onLongScalar(final @NonNull RuntimeTelemetry.Metric scalar) { + Log.d(LOGTAG, "onLongScalar " + scalar); + } + + @Override + public void onStringScalar(final @NonNull RuntimeTelemetry.Metric scalar) { + Log.d(LOGTAG, "onStringScalar " + scalar); + } + } + + private class ExampleActivityDelegate implements GeckoView.ActivityContextDelegate { + public Context getActivityContext() { + return GeckoViewActivity.this; + } + } +} 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..c51ab969c6 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewBottomBehavior.java @@ -0,0 +1,30 @@ +/* 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.View; +import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import org.mozilla.geckoview.GeckoView; + +public class GeckoViewBottomBehavior extends CoordinatorLayout.Behavior { + 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..c09d80509d --- /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 android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.TextView; +import androidx.appcompat.widget.AppCompatEditText; + +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..c165acd3c8 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/NestedGeckoView.java @@ -0,0 +1,169 @@ +/* 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.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.onTouchEventForDetailResult(event) + .accept( + inputResult -> { + mInputResult = inputResult.handledResult(); + 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..e1ad16238c --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SessionActivity.java @@ -0,0 +1,7 @@ +/* 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; + +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..e9229a0b1f --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/SettingsActivity.java @@ -0,0 +1,44 @@ +/* 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.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.preference.PreferenceFragmentCompat; + +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..a7afa51352 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java @@ -0,0 +1,46 @@ +/* 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 androidx.annotation.NonNull; +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..83bd4fa14e --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java @@ -0,0 +1,121 @@ +/* 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 androidx.annotation.Nullable; +import java.util.ArrayList; +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.geckoview.GeckoSessionSettings; +import org.mozilla.geckoview.WebExtension; + +public class TabSessionManager { + private static ArrayList 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() || index < 0) { + 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 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..af4d3a1e98 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarBottomBehavior.java @@ -0,0 +1,64 @@ +/* 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.View; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; +import org.mozilla.geckoview.PanZoomController; + +public class ToolbarBottomBehavior extends CoordinatorLayout.Behavior { + 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..ce074f2ee3 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java @@ -0,0 +1,131 @@ +/* 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.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.GradientDrawable; +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; +import androidx.core.content.ContextCompat; + +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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_camera.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic_camera.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_crash.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_status_logo.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-mdpi/ic_crash.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_camera.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic_camera.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/ic_crash.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_camera.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic_camera.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/ic_crash.png 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 Binary files /dev/null and b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/logo.png 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 @@ + + + + + \ 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 @@ + + + + + + + + + + + \ 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 @@ + + + + + + + + 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 @@ + + + + + + + + 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 @@ + + + + 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 @@ + + + + + + + + + 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..13f2721bcf --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/menu/actions.xml @@ -0,0 +1,19 @@ + +

+ + + + + + + + + + + + + + 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 @@ + + + #3F51B5 + #00a996 + #FFFFFF + #FF4081 + 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 @@ + + + + + + + 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..2dbc2a529c --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml @@ -0,0 +1,152 @@ + + GeckoView Example + GeckoView Example + Enter URL or search keywords... + Username + Password + Clear + Allow access to device storage for "%1$s"? + Allow third parties to access first party storage for "%1$s"? + Share location with "%1$s"? + Allow notifications for "%1$s"? + Share video with "%1$s" + Share audio with "%1$s" + Share video and audio with "%1$s" + Share WebXR displays with "%1$s"? + Allow video to autoplay on "%1$s"? + Allow system media key access for "%1$s"? + Back camera + Front camera + Microphone + Unknown source + + Native + Java + Content (Native) + Tracking Protection + TP exception + Private Browsing + Remote Debugging + Forward + Reload + Settings... + GeckoView Example Crashed + Tap to report to Mozilla. + Ignore + Report + Microphone is on + Camera is on + Camera and microphone are on + New tab + Install Add-on... + Add-on URL: https://addons.mozilla.org/... + Update Add-on + Close tab + Desktop site + Collapse GV + A script on this page is causing your web browser to run slowly + Wait + Stop + Install + Cancel + WebExtension xpi URL + Save as PDF + Print Page + + # Preferences + tracking_protection + true + + javascript_enabled + true + + remote_debugging + true + + dfpi + false + + autoplay + false + + allow_extensions_in_private_browsing + false + + user_agent_override + + + Default + Chrome 80 on Android + Safari 12 on iPhone + + + + Mozilla/5.0 (Linux; Android 10; Z832 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Mobile Safari/537.36 + 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 + + + cookie_banner_handling + cookie_banner_handling_pb + disabled + reject_accept_all + enhanced_tracking_protection + standard + + Disabled + Enabled (standard) + Enabled (strict) + + + disabled + standard + strict + + + Disabled + Enabled (reject all) + Enabled (reject or accept all) + + + disabled + reject_all + reject_accept_all + + + preferred_color_scheme + -1 + + Follow System Preference + Light + Dark + + + -1 + 0 + 1 + + + display_mode + 0 + + Browser + MinimalUi + Standalone + Fullscreen + + + 0 + 1 + 2 + 3 + + + This page is asking you to confirm that you want to leave - data you have entered may not be saved + Are you sure? + Leave Page + Stay on Page + + To display this page, GeckoViewExample must send information that will repeat any action (such as a search or order confirmation) that was performed earlier. + Are you sure? + Resend + Cancel + 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 @@ + + + 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..a29be429a0 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/xml/settings.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + -- cgit v1.2.3