diff options
Diffstat (limited to 'mobile/android/geckoview_example')
44 files changed, 5016 insertions, 0 deletions
diff --git a/mobile/android/geckoview_example/build.gradle b/mobile/android/geckoview_example/build.gradle new file mode 100644 index 0000000000..7f6259fbb7 --- /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.3.0" + implementation "androidx.appcompat:appcompat:1.4.0" + implementation "androidx.preference:preference:1.1.1" + + implementation project(path: ':geckoview') + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + implementation 'com.google.android.material:material:1.4.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 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.mozilla.geckoview_example"> + + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + <uses-permission android:name="android.permission.RECORD_AUDIO"/> + <uses-permission android:name="android.permission.CAMERA"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> + + <application + android:allowBackup="true" + android:label="@string/app_name" + android:supportsRtl="true" + android:usesCleartextTraffic="true" + android:icon="@drawable/logo" + android:name="androidx.multidex.MultiDexApplication"> + <uses-library android:name="android.test.runner" + android:required="false"/> + + <activity + android:configChanges="keyboardHidden|orientation|screenSize" + android:name=".GeckoViewActivity" + android:label="GeckoView Example" + android:launchMode="singleTop" + android:theme="@style/Theme.AppCompat.Light.NoActionBar" + android:exported="true" + android:windowSoftInputMode="stateUnspecified|adjustResize"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + + <category android:name="android.intent.category.LAUNCHER"/> + <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/> + <category android:name="android.intent.category.APP_BROWSER"/> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW"/> + + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> + + <data android:scheme="http"/> + <data android:scheme="https"/> + <data android:scheme="about"/> + <data android:scheme="javascript"/> + </intent-filter> + </activity> + <activity + android:name=".SessionActivity" + android:label="GeckoView Example" + android:theme="@style/Theme.AppCompat.Light.NoActionBar" + android:exported="false" + android:windowSoftInputMode="stateUnspecified|adjustResize"> + </activity> + <activity + android:name=".SettingsActivity" + android:label="Settings" + android:exported="false" + android:theme="@style/Theme.AppCompat.Light.NoActionBar"> + </activity> + + <service + android:name=".ExampleCrashHandler" + android:exported="false" + android:process=":crash"> + </service> + </application> + +</manifest> diff --git a/mobile/android/geckoview_example/src/main/assets/error.html b/mobile/android/geckoview_example/src/main/assets/error.html new file mode 100644 index 0000000000..9d2c426df3 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/assets/error.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> + <title>Boom!</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"> + <style> + body { + background-color: red; + color: white; + font-family: sans; + } + + div.container { + width: 75%; + margin: auto; + text-align: center; + } + </style> +</head> +<body> + <div class="container"> + <h1>Boom!</h1> + <p>Something bad happened...</p> + <p>$ERROR</p> + </div> +</body> +</html> diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java new file mode 100644 index 0000000000..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<PromptResponse> mFileResponse; + private FilePrompt mFilePrompt; + + public BasicGeckoViewPrompt(final Activity activity) { + mActivity = activity; + } + + @Override + public GeckoResult<PromptResponse> onAlertPrompt( + final GeckoSession session, final AlertPrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + final AlertDialog.Builder builder = + new AlertDialog.Builder(activity) + .setTitle(prompt.title) + .setMessage(prompt.message) + .setPositiveButton(android.R.string.ok, /* onClickListener */ null); + GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>(); + createStandardDialog(builder, prompt, res).show(); + return res; + } + + @Override + public GeckoResult<PromptResponse> onButtonPrompt( + final GeckoSession session, final ButtonPrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + final AlertDialog.Builder builder = + new AlertDialog.Builder(activity).setTitle(prompt.title).setMessage(prompt.message); + + GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>(); + + final DialogInterface.OnClickListener listener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + res.complete(prompt.confirm(ButtonPrompt.Type.POSITIVE)); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + res.complete(prompt.confirm(ButtonPrompt.Type.NEGATIVE)); + } else { + res.complete(prompt.dismiss()); + } + } + }; + + builder.setPositiveButton(android.R.string.ok, listener); + builder.setNegativeButton(android.R.string.cancel, listener); + + createStandardDialog(builder, prompt, res).show(); + return res; + } + + @Override + public GeckoResult<PromptResponse> onSharePrompt( + final GeckoSession session, final SharePrompt prompt) { + return GeckoResult.fromValue(prompt.dismiss()); + } + + @Nullable + @Override + public GeckoResult<PromptResponse> onRepostConfirmPrompt( + final GeckoSession session, final RepostConfirmPrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + final AlertDialog.Builder builder = + new AlertDialog.Builder(activity) + .setTitle(R.string.repost_confirm_title) + .setMessage(R.string.repost_confirm_message); + + GeckoResult<PromptResponse> res = new GeckoResult<>(); + + final DialogInterface.OnClickListener listener = + (dialog, which) -> { + if (which == DialogInterface.BUTTON_POSITIVE) { + res.complete(prompt.confirm(AllowOrDeny.ALLOW)); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + res.complete(prompt.confirm(AllowOrDeny.DENY)); + } else { + res.complete(prompt.dismiss()); + } + }; + + builder.setPositiveButton(R.string.repost_confirm_resend, listener); + builder.setNegativeButton(R.string.repost_confirm_cancel, listener); + + createStandardDialog(builder, prompt, res).show(); + return res; + } + + @Nullable + @Override + public GeckoResult<PromptResponse> onCreditCardSave( + @NonNull GeckoSession session, + @NonNull AutocompleteRequest<Autocomplete.CreditCardSaveOption> request) { + Log.i(LOGTAG, "onCreditCardSave " + request.options[0].value); + return null; + } + + @Nullable + @Override + public GeckoResult<PromptResponse> onLoginSave( + @NonNull GeckoSession session, + @NonNull AutocompleteRequest<Autocomplete.LoginSaveOption> request) { + Log.i(LOGTAG, "onLoginSave"); + return GeckoResult.fromValue(request.confirm(request.options[0])); + } + + @Nullable + @Override + public GeckoResult<PromptResponse> onBeforeUnloadPrompt( + final GeckoSession session, final BeforeUnloadPrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + final AlertDialog.Builder builder = + new AlertDialog.Builder(activity) + .setTitle(R.string.before_unload_title) + .setMessage(R.string.before_unload_message); + + GeckoResult<PromptResponse> res = new GeckoResult<>(); + + final DialogInterface.OnClickListener listener = + (dialog, which) -> { + if (which == DialogInterface.BUTTON_POSITIVE) { + res.complete(prompt.confirm(AllowOrDeny.ALLOW)); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + res.complete(prompt.confirm(AllowOrDeny.DENY)); + } else { + res.complete(prompt.dismiss()); + } + }; + + builder.setPositiveButton(R.string.before_unload_leave_page, listener); + builder.setNegativeButton(R.string.before_unload_stay, listener); + + createStandardDialog(builder, prompt, res).show(); + return res; + } + + private int getViewPadding(final AlertDialog.Builder builder) { + final TypedArray attr = + builder + .getContext() + .obtainStyledAttributes(new int[] {android.R.attr.listPreferredItemPaddingLeft}); + final int padding = attr.getDimensionPixelSize(0, 1); + attr.recycle(); + return padding; + } + + private LinearLayout addStandardLayout( + final AlertDialog.Builder builder, final String title, final String msg) { + final ScrollView scrollView = new ScrollView(builder.getContext()); + final LinearLayout container = new LinearLayout(builder.getContext()); + final int horizontalPadding = getViewPadding(builder); + final int verticalPadding = (msg == null || msg.isEmpty()) ? horizontalPadding : 0; + container.setOrientation(LinearLayout.VERTICAL); + container.setPadding( + /* left */ horizontalPadding, /* top */ verticalPadding, + /* right */ horizontalPadding, /* bottom */ verticalPadding); + scrollView.addView(container); + builder.setTitle(title).setMessage(msg).setView(scrollView); + return container; + } + + private AlertDialog createStandardDialog( + final AlertDialog.Builder builder, + final BasePrompt prompt, + final GeckoResult<PromptResponse> response) { + final AlertDialog dialog = builder.create(); + dialog.setOnDismissListener( + new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(final DialogInterface dialog) { + if (!prompt.isComplete()) { + response.complete(prompt.dismiss()); + } + } + }); + return dialog; + } + + @Override + public GeckoResult<PromptResponse> onTextPrompt( + final GeckoSession session, final TextPrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + final LinearLayout container = addStandardLayout(builder, prompt.title, prompt.message); + final EditText editText = new EditText(builder.getContext()); + editText.setText(prompt.defaultValue); + container.addView(editText); + + GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>(); + + builder + .setNegativeButton(android.R.string.cancel, /* listener */ null) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + res.complete(prompt.confirm(editText.getText().toString())); + } + }); + + createStandardDialog(builder, prompt, res).show(); + return res; + } + + @Override + public GeckoResult<PromptResponse> onAuthPrompt( + final GeckoSession session, final AuthPrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + final LinearLayout container = addStandardLayout(builder, prompt.title, prompt.message); + + final int flags = prompt.authOptions.flags; + final int level = prompt.authOptions.level; + final EditText username; + if ((flags & AuthPrompt.AuthOptions.Flags.ONLY_PASSWORD) == 0) { + username = new EditText(builder.getContext()); + username.setHint(R.string.username); + username.setText(prompt.authOptions.username); + container.addView(username); + } else { + username = null; + } + + final EditText password = new EditText(builder.getContext()); + password.setHint(R.string.password); + password.setText(prompt.authOptions.password); + password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + container.addView(password); + + if (level != AuthPrompt.AuthOptions.Level.NONE) { + final ImageView secure = new ImageView(builder.getContext()); + secure.setImageResource(android.R.drawable.ic_lock_lock); + container.addView(secure); + } + + GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>(); + + builder + .setNegativeButton(android.R.string.cancel, /* listener */ null) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + if ((flags & AuthPrompt.AuthOptions.Flags.ONLY_PASSWORD) == 0) { + res.complete( + prompt.confirm(username.getText().toString(), password.getText().toString())); + + } else { + res.complete(prompt.confirm(password.getText().toString())); + } + } + }); + createStandardDialog(builder, prompt, res).show(); + + return res; + } + + private static class ModifiableChoice { + public boolean modifiableSelected; + public String modifiableLabel; + public final ChoicePrompt.Choice choice; + + public ModifiableChoice(ChoicePrompt.Choice c) { + choice = c; + modifiableSelected = choice.selected; + modifiableLabel = choice.label; + } + } + + private void addChoiceItems( + final int type, + final ArrayAdapter<ModifiableChoice> list, + final ChoicePrompt.Choice[] items, + final String indent) { + if (type == ChoicePrompt.Type.MENU) { + for (final ChoicePrompt.Choice item : items) { + list.add(new ModifiableChoice(item)); + } + return; + } + + for (final ChoicePrompt.Choice item : items) { + final ModifiableChoice modItem = new ModifiableChoice(item); + + final ChoicePrompt.Choice[] children = item.items; + + if (indent != null && children == null) { + modItem.modifiableLabel = indent + modItem.modifiableLabel; + } + list.add(modItem); + + if (children != null) { + final String newIndent; + if (type == ChoicePrompt.Type.SINGLE || type == ChoicePrompt.Type.MULTIPLE) { + newIndent = (indent != null) ? indent + '\t' : "\t"; + } else { + newIndent = null; + } + addChoiceItems(type, list, children, newIndent); + } + } + } + + private void onChoicePromptImpl( + final GeckoSession session, + final String title, + final String message, + final int type, + final ChoicePrompt.Choice[] choices, + final ChoicePrompt prompt, + final GeckoResult<PromptResponse> res) { + final Activity activity = mActivity; + if (activity == null) { + res.complete(prompt.dismiss()); + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + addStandardLayout(builder, title, message); + + final ListView list = new ListView(builder.getContext()); + if (type == ChoicePrompt.Type.MULTIPLE) { + list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + } + + final ArrayAdapter<ModifiableChoice> adapter = + new ArrayAdapter<ModifiableChoice>( + builder.getContext(), android.R.layout.simple_list_item_1) { + private static final int TYPE_MENU_ITEM = 0; + private static final int TYPE_MENU_CHECK = 1; + private static final int TYPE_SEPARATOR = 2; + private static final int TYPE_GROUP = 3; + private static final int TYPE_SINGLE = 4; + private static final int TYPE_MULTIPLE = 5; + private static final int TYPE_COUNT = 6; + + private LayoutInflater mInflater; + private View mSeparator; + + @Override + public int getViewTypeCount() { + return TYPE_COUNT; + } + + @Override + public int getItemViewType(final int position) { + final ModifiableChoice item = getItem(position); + if (item.choice.separator) { + return TYPE_SEPARATOR; + } else if (type == ChoicePrompt.Type.MENU) { + return item.modifiableSelected ? TYPE_MENU_CHECK : TYPE_MENU_ITEM; + } else if (item.choice.items != null) { + return TYPE_GROUP; + } else if (type == ChoicePrompt.Type.SINGLE) { + return TYPE_SINGLE; + } else if (type == ChoicePrompt.Type.MULTIPLE) { + return TYPE_MULTIPLE; + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean isEnabled(final int position) { + final ModifiableChoice item = getItem(position); + return !item.choice.separator + && !item.choice.disabled + && ((type != ChoicePrompt.Type.SINGLE && type != ChoicePrompt.Type.MULTIPLE) + || item.choice.items == null); + } + + @Override + public View getView(final int position, View view, final ViewGroup parent) { + final int itemType = getItemViewType(position); + final int layoutId; + if (itemType == TYPE_SEPARATOR) { + if (mSeparator == null) { + mSeparator = new View(getContext()); + mSeparator.setLayoutParams( + new ListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2, itemType)); + final TypedArray attr = + getContext().obtainStyledAttributes(new int[] {android.R.attr.listDivider}); + mSeparator.setBackgroundResource(attr.getResourceId(0, 0)); + attr.recycle(); + } + return mSeparator; + } else if (itemType == TYPE_MENU_ITEM) { + layoutId = android.R.layout.simple_list_item_1; + } else if (itemType == TYPE_MENU_CHECK) { + layoutId = android.R.layout.simple_list_item_checked; + } else if (itemType == TYPE_GROUP) { + layoutId = android.R.layout.preference_category; + } else if (itemType == TYPE_SINGLE) { + layoutId = android.R.layout.simple_list_item_single_choice; + } else if (itemType == TYPE_MULTIPLE) { + layoutId = android.R.layout.simple_list_item_multiple_choice; + } else { + throw new UnsupportedOperationException(); + } + + if (view == null) { + if (mInflater == null) { + mInflater = LayoutInflater.from(builder.getContext()); + } + view = mInflater.inflate(layoutId, parent, false); + } + + final ModifiableChoice item = getItem(position); + final TextView text = (TextView) view; + text.setEnabled(!item.choice.disabled); + text.setText(item.modifiableLabel); + if (view instanceof CheckedTextView) { + final boolean selected = item.modifiableSelected; + if (itemType == TYPE_MULTIPLE) { + list.setItemChecked(position, selected); + } else { + ((CheckedTextView) view).setChecked(selected); + } + } + return view; + } + }; + addChoiceItems(type, adapter, choices, /* indent */ null); + + list.setAdapter(adapter); + builder.setView(list); + + final AlertDialog dialog; + if (type == ChoicePrompt.Type.SINGLE || type == ChoicePrompt.Type.MENU) { + dialog = createStandardDialog(builder, prompt, res); + list.setOnItemClickListener( + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick( + final AdapterView<?> parent, final View v, final int position, final long id) { + final ModifiableChoice item = adapter.getItem(position); + if (type == ChoicePrompt.Type.MENU) { + final ChoicePrompt.Choice[] children = item.choice.items; + if (children != null) { + // Show sub-menu. + dialog.setOnDismissListener(null); + dialog.dismiss(); + onChoicePromptImpl( + session, item.modifiableLabel, /* msg */ null, type, children, prompt, res); + return; + } + } + res.complete(prompt.confirm(item.choice)); + dialog.dismiss(); + } + }); + } else if (type == ChoicePrompt.Type.MULTIPLE) { + list.setOnItemClickListener( + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick( + final AdapterView<?> parent, final View v, final int position, final long id) { + final ModifiableChoice item = adapter.getItem(position); + item.modifiableSelected = ((CheckedTextView) v).isChecked(); + } + }); + builder + .setNegativeButton(android.R.string.cancel, /* listener */ null) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + final int len = adapter.getCount(); + ArrayList<String> items = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + final ModifiableChoice item = adapter.getItem(i); + if (item.modifiableSelected) { + items.add(item.choice.id); + } + } + res.complete(prompt.confirm(items.toArray(new String[items.size()]))); + } + }); + dialog = createStandardDialog(builder, prompt, res); + } else { + throw new UnsupportedOperationException(); + } + dialog.show(); + + 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<PromptResponse> onChoicePrompt( + final GeckoSession session, final ChoicePrompt prompt) { + final GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>(); + onChoicePromptImpl( + session, prompt.title, prompt.message, prompt.type, prompt.choices, prompt, res); + return res; + } + + private static int parseColor(final String value, final int def) { + try { + return Color.parseColor(value); + } catch (final IllegalArgumentException e) { + return def; + } + } + + @Override + public GeckoResult<PromptResponse> onColorPrompt( + final GeckoSession session, final ColorPrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + addStandardLayout(builder, prompt.title, /* msg */ null); + + final int initial = parseColor(prompt.defaultValue, /* def */ 0); + final ArrayAdapter<Integer> adapter = + new ArrayAdapter<Integer>(builder.getContext(), android.R.layout.simple_list_item_1) { + private LayoutInflater mInflater; + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public int getItemViewType(final int position) { + return (getItem(position) == initial) ? 1 : 0; + } + + @Override + public View getView(final int position, View view, final ViewGroup parent) { + if (mInflater == null) { + mInflater = LayoutInflater.from(builder.getContext()); + } + final int color = getItem(position); + if (view == null) { + view = + mInflater.inflate( + (color == initial) + ? android.R.layout.simple_list_item_checked + : android.R.layout.simple_list_item_1, + parent, + false); + } + view.setBackgroundResource(android.R.drawable.editbox_background); + view.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY); + return view; + } + }; + + adapter.addAll( + 0xffff4444 /* holo_red_light */, + 0xffcc0000 /* holo_red_dark */, + 0xffffbb33 /* holo_orange_light */, + 0xffff8800 /* holo_orange_dark */, + 0xff99cc00 /* holo_green_light */, + 0xff669900 /* holo_green_dark */, + 0xff33b5e5 /* holo_blue_light */, + 0xff0099cc /* holo_blue_dark */, + 0xffaa66cc /* holo_purple */, + 0xffffffff /* white */, + 0xffaaaaaa /* lighter_gray */, + 0xff555555 /* darker_gray */, + 0xff000000 /* black */); + + final ListView list = new ListView(builder.getContext()); + list.setAdapter(adapter); + builder.setView(list); + + GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>(); + + final AlertDialog dialog = createStandardDialog(builder, prompt, res); + list.setOnItemClickListener( + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick( + final AdapterView<?> parent, final View v, final int position, final long id) { + res.complete( + prompt.confirm(String.format("#%06x", 0xffffff & adapter.getItem(position)))); + dialog.dismiss(); + } + }); + dialog.show(); + + return res; + } + + private static Date parseDate( + final SimpleDateFormat formatter, final String value, final boolean defaultToNow) { + try { + if (value != null && !value.isEmpty()) { + return formatter.parse(value); + } + } catch (final ParseException e) { + } + return defaultToNow ? new Date() : null; + } + + @SuppressWarnings("deprecation") + private static void setTimePickerTime(final TimePicker picker, final Calendar cal) { + if (Build.VERSION.SDK_INT >= 23) { + picker.setHour(cal.get(Calendar.HOUR_OF_DAY)); + picker.setMinute(cal.get(Calendar.MINUTE)); + } else { + picker.setCurrentHour(cal.get(Calendar.HOUR_OF_DAY)); + picker.setCurrentMinute(cal.get(Calendar.MINUTE)); + } + } + + @SuppressWarnings("deprecation") + private static void setCalendarTime(final Calendar cal, final TimePicker picker) { + if (Build.VERSION.SDK_INT >= 23) { + cal.set(Calendar.HOUR_OF_DAY, picker.getHour()); + cal.set(Calendar.MINUTE, picker.getMinute()); + } else { + cal.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour()); + cal.set(Calendar.MINUTE, picker.getCurrentMinute()); + } + } + + @Override + public GeckoResult<PromptResponse> onDateTimePrompt( + final GeckoSession session, final DateTimePrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + final String format; + if (prompt.type == DateTimePrompt.Type.DATE) { + format = "yyyy-MM-dd"; + } else if (prompt.type == DateTimePrompt.Type.MONTH) { + format = "yyyy-MM"; + } else if (prompt.type == DateTimePrompt.Type.WEEK) { + format = "yyyy-'W'ww"; + } else if (prompt.type == DateTimePrompt.Type.TIME) { + format = "HH:mm"; + } else if (prompt.type == DateTimePrompt.Type.DATETIME_LOCAL) { + format = "yyyy-MM-dd'T'HH:mm"; + } else { + throw new UnsupportedOperationException(); + } + + final SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.ROOT); + final Date minDate = parseDate(formatter, prompt.minValue, /* defaultToNow */ false); + final Date maxDate = parseDate(formatter, prompt.maxValue, /* defaultToNow */ false); + final Date date = parseDate(formatter, prompt.defaultValue, /* defaultToNow */ true); + final Calendar cal = formatter.getCalendar(); + cal.setTime(date); + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + final LayoutInflater inflater = LayoutInflater.from(builder.getContext()); + final DatePicker datePicker; + if (prompt.type == DateTimePrompt.Type.DATE + || prompt.type == DateTimePrompt.Type.MONTH + || prompt.type == DateTimePrompt.Type.WEEK + || prompt.type == DateTimePrompt.Type.DATETIME_LOCAL) { + final int resId = + builder + .getContext() + .getResources() + .getIdentifier("date_picker_dialog", "layout", "android"); + DatePicker picker = null; + if (resId != 0) { + try { + picker = (DatePicker) inflater.inflate(resId, /* root */ null); + } catch (final ClassCastException | InflateException e) { + } + } + if (picker == null) { + picker = new DatePicker(builder.getContext()); + } + picker.init( + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH), /* listener */ + null); + if (minDate != null) { + picker.setMinDate(minDate.getTime()); + } + if (maxDate != null) { + picker.setMaxDate(maxDate.getTime()); + } + datePicker = picker; + } else { + datePicker = null; + } + + final TimePicker timePicker; + if (prompt.type == DateTimePrompt.Type.TIME + || prompt.type == DateTimePrompt.Type.DATETIME_LOCAL) { + final int resId = + builder + .getContext() + .getResources() + .getIdentifier("time_picker_dialog", "layout", "android"); + TimePicker picker = null; + if (resId != 0) { + try { + picker = (TimePicker) inflater.inflate(resId, /* root */ null); + } catch (final ClassCastException | InflateException e) { + } + } + if (picker == null) { + picker = new TimePicker(builder.getContext()); + } + setTimePickerTime(picker, cal); + picker.setIs24HourView(DateFormat.is24HourFormat(builder.getContext())); + timePicker = picker; + } else { + timePicker = null; + } + + final LinearLayout container = addStandardLayout(builder, prompt.title, /* msg */ null); + container.setPadding(/* left */ 0, /* top */ 0, /* right */ 0, /* bottom */ 0); + if (datePicker != null) { + container.addView(datePicker); + } + if (timePicker != null) { + container.addView(timePicker); + } + + GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>(); + + final DialogInterface.OnClickListener listener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (which == DialogInterface.BUTTON_NEUTRAL) { + // Clear + res.complete(prompt.confirm("")); + return; + } + if (datePicker != null) { + cal.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth()); + } + if (timePicker != null) { + setCalendarTime(cal, timePicker); + } + res.complete(prompt.confirm(formatter.format(cal.getTime()))); + } + }; + builder + .setNegativeButton(android.R.string.cancel, /* listener */ null) + .setNeutralButton(R.string.clear_field, listener) + .setPositiveButton(android.R.string.ok, listener); + + 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<PromptResponse> onFilePrompt(GeckoSession session, FilePrompt prompt) { + final Activity activity = mActivity; + if (activity == null) { + return GeckoResult.fromValue(prompt.dismiss()); + } + + // Merge all given MIME types into one, using wildcard if needed. + String mimeType = null; + String mimeSubtype = null; + if (prompt.mimeTypes != null) { + for (final String rawType : prompt.mimeTypes) { + final String normalizedType = rawType.trim().toLowerCase(Locale.ROOT); + final int len = normalizedType.length(); + int slash = normalizedType.indexOf('/'); + if (slash < 0) { + slash = len; + } + final String newType = normalizedType.substring(0, slash); + final String newSubtype = normalizedType.substring(Math.min(slash + 1, len)); + if (mimeType == null) { + mimeType = newType; + } else if (!mimeType.equals(newType)) { + mimeType = "*"; + } + if (mimeSubtype == null) { + mimeSubtype = newSubtype; + } else if (!mimeSubtype.equals(newSubtype)) { + mimeSubtype = "*"; + } + } + } + + final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType( + (mimeType != null ? mimeType : "*") + '/' + (mimeSubtype != null ? mimeSubtype : "*")); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + if (Build.VERSION.SDK_INT >= 18 && prompt.type == FilePrompt.Type.MULTIPLE) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } + if (Build.VERSION.SDK_INT >= 19 && prompt.mimeTypes.length > 0) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, prompt.mimeTypes); + } + + GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>(); + + try { + mFileResponse = res; + mFilePrompt = prompt; + activity.startActivityForResult(intent, filePickerRequestCode); + } catch (final ActivityNotFoundException e) { + Log.e(LOGTAG, "Cannot launch activity", e); + return GeckoResult.fromValue(prompt.dismiss()); + } + + return res; + } + + public void onFileCallbackResult(final int resultCode, final Intent data) { + if (mFileResponse == null) { + return; + } + + final GeckoResult<PromptResponse> res = mFileResponse; + mFileResponse = null; + + final FilePrompt prompt = mFilePrompt; + mFilePrompt = null; + + if (resultCode != Activity.RESULT_OK || data == null) { + res.complete(prompt.dismiss()); + return; + } + + final Uri uri = data.getData(); + final ClipData clip = data.getClipData(); + + if (prompt.type == FilePrompt.Type.SINGLE + || (prompt.type == FilePrompt.Type.MULTIPLE && clip == null)) { + res.complete(prompt.confirm(mActivity, uri)); + } else if (prompt.type == FilePrompt.Type.MULTIPLE) { + if (clip == null) { + Log.w(LOGTAG, "No selected file"); + res.complete(prompt.dismiss()); + return; + } + final int count = clip.getItemCount(); + final ArrayList<Uri> uris = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + uris.add(clip.getItemAt(i).getUri()); + } + res.complete(prompt.confirm(mActivity, uris.toArray(new Uri[uris.size()]))); + } + } + + public GeckoResult<Integer> onPermissionPrompt( + final GeckoSession session, + final String title, + final GeckoSession.PermissionDelegate.ContentPermission perm) { + final Activity activity = mActivity; + final GeckoResult<Integer> 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<SlowScriptResponse> reportAction) { + final Activity activity = mActivity; + if (activity == null) { + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder + .setTitle(title) + .setNegativeButton( + activity.getString(R.string.wait), + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + reportAction.complete(SlowScriptResponse.CONTINUE); + } + }) + .setPositiveButton( + activity.getString(R.string.stop), + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + reportAction.complete(SlowScriptResponse.STOP); + } + }); + + final AlertDialog dialog = builder.create(); + dialog.show(); + } + + private Spinner addMediaSpinner( + final Context context, + final ViewGroup container, + final MediaSource[] sources, + final String[] sourceNames) { + final ArrayAdapter<MediaSource> adapter = + new ArrayAdapter<MediaSource>(context, android.R.layout.simple_spinner_item) { + private View convertView(final int position, final View view) { + if (view != null) { + final MediaSource item = getItem(position); + ((TextView) view).setText(sourceNames != null ? sourceNames[position] : item.name); + } + return view; + } + + @Override + public View getView(final int position, View view, final ViewGroup parent) { + return convertView(position, super.getView(position, view, parent)); + } + + @Override + public View getDropDownView(final int position, final View view, final ViewGroup parent) { + return convertView(position, super.getDropDownView(position, view, parent)); + } + }; + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + adapter.addAll(sources); + + final Spinner spinner = new Spinner(context); + spinner.setAdapter(adapter); + spinner.setSelection(0); + container.addView(spinner); + return spinner; + } + + public void onMediaPrompt( + final GeckoSession session, + final String title, + final MediaSource[] video, + final MediaSource[] audio, + final String[] videoNames, + final String[] audioNames, + final GeckoSession.PermissionDelegate.MediaCallback callback) { + final Activity activity = mActivity; + if (activity == null || (video == null && audio == null)) { + callback.reject(); + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + final LinearLayout container = addStandardLayout(builder, title, /* msg */ null); + + final Spinner videoSpinner; + if (video != null) { + videoSpinner = addMediaSpinner(builder.getContext(), container, video, videoNames); + } else { + videoSpinner = null; + } + + final Spinner audioSpinner; + if (audio != null) { + audioSpinner = addMediaSpinner(builder.getContext(), container, audio, audioNames); + } else { + audioSpinner = null; + } + + builder + .setNegativeButton(android.R.string.cancel, /* listener */ null) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + final MediaSource video = + (videoSpinner != null) ? (MediaSource) videoSpinner.getSelectedItem() : null; + final MediaSource audio = + (audioSpinner != null) ? (MediaSource) audioSpinner.getSelectedItem() : null; + callback.grant(video, audio); + } + }); + + final AlertDialog dialog = builder.create(); + dialog.setOnDismissListener( + new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(final DialogInterface dialog) { + callback.reject(); + } + }); + dialog.show(); + } + + public void onMediaPrompt( + final GeckoSession session, + final String title, + final MediaSource[] video, + final MediaSource[] audio, + final GeckoSession.PermissionDelegate.MediaCallback callback) { + onMediaPrompt(session, title, video, audio, null, null, callback); + } + + @Override + public GeckoResult<PromptResponse> onPopupPrompt( + final GeckoSession session, final PopupPrompt prompt) { + return GeckoResult.fromValue(prompt.confirm(AllowOrDeny.ALLOW)); + } +} diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ExampleCrashHandler.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ExampleCrashHandler.java new file mode 100644 index 0000000000..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..1268bfd2ba --- /dev/null +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java @@ -0,0 +1,2483 @@ +/* -*- 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.Intent; +import android.content.IntentSender; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.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.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<Image, Bitmap> mBitmapCache = new LruCache<>(5); + private GeckoRuntime mRuntime; + private WebExtension.Action mDefaultAction; + private TabSessionManager mTabManager; + + private WeakReference<WebExtensionDelegate> mExtensionDelegate; + + @Nullable + @Override + public GeckoResult<AllowOrDeny> onInstallPrompt(final @NonNull WebExtension extension) { + return GeckoResult.allow(); + } + + @Nullable + @Override + public GeckoResult<AllowOrDeny> onUpdatePrompt( + @NonNull WebExtension currentlyInstalled, + @NonNull WebExtension updatedExtension, + @NonNull String[] newPermissions, + @NonNull String[] newOrigins) { + return GeckoResult.allow(); + } + + @Nullable + @Override + public GeckoResult<AllowOrDeny> 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<GeckoSession> onNewTab( + WebExtension source, WebExtension.CreateTabDetails details) { + WebExtensionDelegate delegate = mExtensionDelegate.get(); + if (delegate == null) { + return GeckoResult.fromValue(null); + } + return GeckoResult.fromValue(delegate.openNewTab(details)); + } + + @Override + public GeckoResult<AllowOrDeny> onCloseTab(WebExtension extension, GeckoSession session) { + final WebExtensionDelegate delegate = mExtensionDelegate.get(); + if (delegate == null) { + return GeckoResult.deny(); + } + + final TabSession tabSession = mTabManager.getSession(session); + if (tabSession != null) { + delegate.closeTab(tabSession); + } + + return GeckoResult.allow(); + } + + @Override + public GeckoResult<AllowOrDeny> 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<GeckoSession> togglePopup(boolean force) { + WebExtensionDelegate extensionDelegate = mExtensionDelegate.get(); + if (extensionDelegate == null) { + return null; + } + + GeckoSession session = extensionDelegate.toggleBrowserActionPopup(false); + if (session == null) { + return null; + } + + return GeckoResult.fromValue(session); + } + + @Override + public GeckoResult<GeckoSession> onTogglePopup( + final @NonNull WebExtension extension, final @NonNull WebExtension.Action action) { + return togglePopup(false); + } + + @Override + public GeckoResult<GeckoSession> onOpenPopup( + final @NonNull WebExtension extension, final @NonNull WebExtension.Action action) { + return togglePopup(true); + } + + private WebExtension.Action actionFor(TabSession session) { + if (session.action == null) { + return mDefaultAction; + } else { + return session.action.withDefault(mDefaultAction); + } + } + + private void updateAction(WebExtension.Action resolved) { + WebExtensionDelegate extensionDelegate = mExtensionDelegate.get(); + if (extensionDelegate == null) { + return; + } + + if (resolved == null || resolved.enabled == null || !resolved.enabled) { + extensionDelegate.onActionButton(null); + return; + } + + if (resolved.icon != null) { + if (mBitmapCache.get(resolved.icon) != null) { + extensionDelegate.onActionButton( + new ActionButton( + mBitmapCache.get(resolved.icon), + resolved.badgeText, + resolved.badgeTextColor, + resolved.badgeBackgroundColor)); + } else { + resolved + .icon + .getBitmap(100) + .accept( + bitmap -> { + mBitmapCache.put(resolved.icon, bitmap); + extensionDelegate.onActionButton( + new ActionButton( + bitmap, + resolved.badgeText, + resolved.badgeTextColor, + resolved.badgeBackgroundColor)); + }); + } + } else { + extensionDelegate.onActionButton(null); + } + } + + public void onClicked(TabSession session) { + WebExtension.Action action = actionFor(session); + if (action != null) { + action.click(); + } + } + + public void setExtensionDelegate(WebExtensionDelegate delegate) { + mExtensionDelegate = new WeakReference<>(delegate); + } + + @Override + public void onCurrentSession(TabSession session) { + if (mDefaultAction == null) { + // No action was ever defined, so nothing to do + return; + } + + if (session.action != null) { + updateAction(session.action.withDefault(mDefaultAction)); + } else { + updateAction(mDefaultAction); + } + } + + public GeckoResult<Void> unregisterExtension() { + if (extension == null) { + return GeckoResult.fromValue(null); + } + + mTabManager.unregisterWebExtension(); + + return mRuntime + .getWebExtensionController() + .uninstall(extension) + .accept( + (unused) -> { + extension = null; + mDefaultAction = null; + updateAction(null); + }); + } + + public GeckoResult<WebExtension> updateExtension() { + if (extension == null) { + return GeckoResult.fromValue(null); + } + + return mRuntime + .getWebExtensionController() + .update(extension) + .map( + newExtension -> { + registerExtension(newExtension); + return newExtension; + }); + } + + public void registerExtension(WebExtension extension) { + extension.setActionDelegate(this); + extension.setTabDelegate(this); + mTabManager.setWebExtensionDelegates(extension, this, this); + this.extension = extension; + } + + private void refreshExtensionList() { + mRuntime + .getWebExtensionController() + .list() + .accept( + extensions -> { + for (final WebExtension extension : extensions) { + registerExtension(extension); + } + }); + } + + public WebExtensionManager(GeckoRuntime runtime, TabSessionManager tabManager) { + mTabManager = tabManager; + mRuntime = runtime; + refreshExtensionList(); + } +} + +public class GeckoViewActivity extends AppCompatActivity + implements ToolbarLayout.TabListener, + WebExtensionDelegate, + SharedPreferences.OnSharedPreferenceChangeListener { + private static final String LOGTAG = "GeckoViewActivity"; + private static final String FULL_ACCESSIBILITY_TREE_EXTRA = "full_accessibility_tree"; + private static final String SEARCH_URI_BASE = "https://www.google.com/search?q="; + private static final String ACTION_SHUTDOWN = "org.mozilla.geckoview_example.SHUTDOWN"; + private static final String CHANNEL_ID = "GeckoViewExample"; + private static final int REQUEST_FILE_PICKER = 1; + private static final int REQUEST_PERMISSIONS = 2; + private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 3; + + private static GeckoRuntime sGeckoRuntime; + + private static WebExtensionManager sExtensionManager; + + private TabSessionManager mTabSessionManager; + private GeckoView mGeckoView; + private boolean mFullAccessibilityTree; + private boolean mUsePrivateBrowsing; + private boolean mCollapsed; + private boolean mKillProcessOnDestroy; + private boolean mDesktopMode; + + private TabSession mPopupSession; + private View mPopupView; + private ContentPermission mTrackingProtectionPermission; + + private boolean mShowNotificationsRejected; + private ArrayList<String> mAcceptedPersistentStorage = new ArrayList<String>(); + + private ToolbarLayout mToolbarView; + private String mCurrentUri; + private boolean mCanGoBack; + private boolean mCanGoForward; + private boolean mFullScreen; + + private HashMap<String, Integer> mNotificationIDMap = new HashMap<>(); + private int mLastID = 100; + + private ProgressBar mProgressView; + + private LinkedList<WebResponse> mPendingDownloads = new LinkedList<>(); + + private int mNextActivityResultCode = 10; + private HashMap<Integer, GeckoResult<Intent>> mPendingActivityResult = new HashMap<>(); + + private LocationView.CommitListener mCommitListener = + new LocationView.CommitListener() { + @Override + public void onCommit(String text) { + if (text.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<Setting<?>> SETTINGS = new ArrayList<>(); + + private abstract class Setting<T> { + private int mKey; + private int mDefaultKey; + private final boolean mReloadCurrentSession; + private T mValue; + + public Setting(final int key, final int defaultValueKey, final boolean reloadCurrentSession) { + mKey = key; + mDefaultKey = defaultValueKey; + mReloadCurrentSession = reloadCurrentSession; + + SETTINGS.add(this); + } + + public void onPrefChange(SharedPreferences pref) { + final T defaultValue = getDefaultValue(mDefaultKey, getResources()); + final String key = getResources().getString(this.mKey); + final T value = getValue(key, defaultValue, pref); + if (!value().equals(value)) { + setValue(value); + } + } + + private void setValue(final T newValue) { + mValue = newValue; + for (final TabSession session : mTabSessionManager.getSessions()) { + setValue(session.getSettings(), value()); + } + if (sGeckoRuntime != null) { + setValue(sGeckoRuntime.getSettings(), value()); + if (sExtensionManager != null) { + setValue(sGeckoRuntime.getWebExtensionController(), value()); + } + } + + final GeckoSession current = mTabSessionManager.getCurrentSession(); + if (mReloadCurrentSession && current != null) { + current.reload(); + } + } + + public T value() { + return mValue == null ? getDefaultValue(mDefaultKey, getResources()) : mValue; + } + + protected abstract T getDefaultValue(final int key, final Resources res); + + protected abstract T getValue( + final String key, final T defaultValue, final SharedPreferences preferences); + + /** Override one of these to define the behavior when this setting changes. */ + protected void setValue(final GeckoSessionSettings settings, final T value) {} + + protected void setValue(final GeckoRuntimeSettings settings, final T value) {} + + protected void setValue(final WebExtensionController controller, final T value) {} + } + + private class StringSetting extends Setting<String> { + public StringSetting(final int key, final int defaultValueKey) { + this(key, defaultValueKey, false); + } + + public StringSetting( + final int key, final int defaultValueKey, final boolean reloadCurrentSession) { + super(key, defaultValueKey, reloadCurrentSession); + } + + @Override + protected String getDefaultValue(int key, final Resources res) { + return res.getString(key); + } + + @Override + public String getValue( + final String key, final String defaultValue, final SharedPreferences preferences) { + return preferences.getString(key, defaultValue); + } + } + + private class BooleanSetting extends Setting<Boolean> { + public BooleanSetting(final int key, final int defaultValueKey) { + this(key, defaultValueKey, false); + } + + public BooleanSetting( + final int key, final int defaultValueKey, final boolean reloadCurrentSession) { + super(key, defaultValueKey, reloadCurrentSession); + } + + @Override + protected Boolean getDefaultValue(int key, Resources res) { + return res.getBoolean(key); + } + + @Override + public Boolean getValue( + final String key, final Boolean defaultValue, final SharedPreferences preferences) { + return preferences.getBoolean(key, defaultValue); + } + } + + private class IntSetting extends Setting<Integer> { + public IntSetting(final int key, final int defaultValueKey) { + this(key, defaultValueKey, false); + } + + public IntSetting( + final int key, final int defaultValueKey, final boolean reloadCurrentSession) { + super(key, defaultValueKey, reloadCurrentSession); + } + + @Override + protected Integer getDefaultValue(int key, Resources res) { + return res.getInteger(key); + } + + @Override + public Integer getValue( + final String key, final Integer defaultValue, final SharedPreferences preferences) { + return Integer.parseInt(preferences.getString(key, Integer.toString(defaultValue))); + } + } + + private final IntSetting mDisplayMode = + new IntSetting(R.string.key_display_mode, R.integer.display_mode_default) { + @Override + public void setValue(final GeckoSessionSettings settings, final Integer value) { + settings.setDisplayMode(value); + } + }; + + private final IntSetting mPreferredColorScheme = + new IntSetting( + R.string.key_preferred_color_scheme, + R.integer.preferred_color_scheme_default, + /* reloadCurrentSession */ true) { + @Override + public void setValue(final GeckoRuntimeSettings settings, final Integer value) { + settings.setPreferredColorScheme(value); + } + }; + + private final StringSetting mUserAgent = + new StringSetting( + R.string.key_user_agent_override, + R.string.user_agent_override_default, + /* reloadCurrentSession */ true) { + @Override + public void setValue(final GeckoSessionSettings settings, final String value) { + settings.setUserAgentOverride(value.isEmpty() ? null : value); + } + }; + + private final BooleanSetting mRemoteDebugging = + new BooleanSetting(R.string.key_remote_debugging, R.bool.remote_debugging_default) { + @Override + public void setValue(final GeckoRuntimeSettings settings, final Boolean value) { + settings.setRemoteDebuggingEnabled(value); + } + }; + + private final BooleanSetting mJavascriptEnabled = + new BooleanSetting( + R.string.key_javascript_enabled, + R.bool.javascript_enabled_default, + /* reloadCurrentSession */ true) { + @Override + public void setValue(final GeckoRuntimeSettings settings, final Boolean value) { + settings.setJavaScriptEnabled(value); + } + }; + + private final BooleanSetting mTrackingProtection = + new BooleanSetting(R.string.key_tracking_protection, R.bool.tracking_protection_default) { + @Override + public void setValue(final GeckoRuntimeSettings settings, final Boolean value) { + mTabSessionManager.setUseTrackingProtection(value); + settings.getContentBlocking().setStrictSocialTrackingProtection(value); + } + }; + + private final StringSetting mEnhancedTrackingProtection = + new StringSetting( + R.string.key_enhanced_tracking_protection, + R.string.enhanced_tracking_protection_default) { + @Override + public void setValue(final GeckoRuntimeSettings settings, final String value) { + int etpLevel; + switch (value) { + case "disabled": + etpLevel = ContentBlocking.EtpLevel.NONE; + break; + case "standard": + etpLevel = ContentBlocking.EtpLevel.DEFAULT; + break; + case "strict": + etpLevel = ContentBlocking.EtpLevel.STRICT; + break; + default: + throw new RuntimeException("Invalid ETP level: " + value); + } + + settings.getContentBlocking().setEnhancedTrackingProtectionLevel(etpLevel); + } + }; + + private final 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); + + 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<GeckoSession> 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<WebResponse> 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<Intent> result = new GeckoResult<>(); + try { + final int code = mNextActivityResultCode++; + mPendingActivityResult.put(code, result); + GeckoViewActivity.this.startIntentSenderForResult( + pendingIntent.getIntentSender(), code, null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + result.completeExceptionally(e); + } + return result; + }); + } + + sExtensionManager.setExtensionDelegate(this); + + if (savedInstanceState == null) { + TabSession session = getIntent().getParcelableExtra("session"); + if (session != null) { + connectSession(session); + + if (!session.isOpen()) { + session.open(sGeckoRuntime); + } + + mFullAccessibilityTree = session.getSettings().getFullAccessibilityTree(); + + mTabSessionManager.addSession(session); + session.open(sGeckoRuntime); + setGeckoViewSession(session); + } else { + session = createSession(); + session.open(sGeckoRuntime); + mTabSessionManager.setCurrentSession(session); + mGeckoView.setSession(session); + sGeckoRuntime.getWebExtensionController().setTabActive(session, true); + } + loadFromIntent(getIntent()); + } + + mGeckoView.setDynamicToolbarMaxHeight(findViewById(R.id.toolbar).getLayoutParams().height); + + mToolbarView.getLocationView().setCommitListener(mCommitListener); + mToolbarView.updateTabCount(); + } + + private void openSettingsActivity() { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + } + + @Override + public TabSession getSession(GeckoSession session) { + return mTabSessionManager.getSession(session); + } + + @Override + public TabSession getCurrentSession() { + return mTabSessionManager.getCurrentSession(); + } + + @Override + public void onActionButton(ActionButton button) { + mToolbarView.setBrowserActionButton(button); + } + + @Override + public GeckoSession toggleBrowserActionPopup(boolean force) { + if (mPopupSession == null) { + openPopupSession(); + } + + ViewGroup.LayoutParams params = mPopupView.getLayoutParams(); + boolean shouldShow = force || params.width == 0; + 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.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; + 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()); + } + }); + } + + @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<Intent> result = mPendingActivityResult.remove(requestCode); + + if (resultCode == Activity.RESULT_OK) { + result.complete(data); + } else { + result.completeExceptionally(new RuntimeException("Unknown error")); + } + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void onRequestPermissionsResult( + final int requestCode, final String[] permissions, final int[] grantResults) { + if (requestCode == REQUEST_PERMISSIONS) { + final ExamplePermissionDelegate permission = + (ExamplePermissionDelegate) + mTabSessionManager.getCurrentSession().getPermissionDelegate(); + permission.onRequestPermissionsResult(permissions, grantResults); + } else if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + continueDownloads(); + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + private void continueDownloads() { + final LinkedList<WebResponse> downloads = mPendingDownloads; + mPendingDownloads = new LinkedList<>(); + + for (final WebResponse response : downloads) { + downloadFile(response); + } + } + + private void downloadFile(final WebResponse response) { + if (response.body == null) { + return; + } + + if (ContextCompat.checkSelfPermission( + GeckoViewActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + mPendingDownloads.add(response); + ActivityCompat.requestPermissions( + GeckoViewActivity.this, + new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, + REQUEST_WRITE_EXTERNAL_STORAGE); + return; + } + + final String filename = getFileName(response); + + try { + String downloadsPath = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + .getAbsolutePath() + + "/" + + filename; + + 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<String> mVisitedURLs; + + private ExampleHistoryDelegate() { + mVisitedURLs = new HashSet<String>(); + } + + @Override + public GeckoResult<Boolean> onVisited( + GeckoSession session, String url, String lastVisitedURL, int flags) { + Log.i(LOGTAG, "Visited URL: " + url); + + mVisitedURLs.add(url); + return GeckoResult.fromValue(true); + } + + @Override + public GeckoResult<boolean[]> getVisited(GeckoSession session, String[] urls) { + boolean[] visited = new boolean[urls.length]; + for (int i = 0; i < urls.length; i++) { + visited[i] = mVisitedURLs.contains(urls[i]); + } + return GeckoResult.fromValue(visited); + } + + @Override + public void onHistoryStateChange( + final GeckoSession session, final GeckoSession.HistoryDelegate.HistoryList state) { + Log.i(LOGTAG, "History state updated"); + } + } + + private class ExampleAutocompleteStorageDelegate implements Autocomplete.StorageDelegate { + private Map<String, Autocomplete.LoginEntry> mStorage = new HashMap<>(); + + @Nullable + @Override + public GeckoResult<Autocomplete.LoginEntry[]> 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<AllowOrDeny> onOrientationLock(@NonNull int aOrientation) { + setRequestedOrientation(aOrientation); + return GeckoResult.allow(); + } + } + + 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<SlowScriptResponse> onSlowScript( + final GeckoSession geckoSession, final String scriptFileName) { + BasicGeckoViewPrompt prompt = + (BasicGeckoViewPrompt) mTabSessionManager.getCurrentSession().getPromptDelegate(); + if (prompt != null) { + GeckoResult<SlowScriptResponse> result = new GeckoResult<SlowScriptResponse>(); + if (!activeAlert) { + activeAlert = true; + prompt.onSlowScriptPrompt(geckoSession, getString(R.string.slow_script), result); + } + return result.then( + value -> { + activeAlert = false; + return GeckoResult.fromValue(value); + }); + } + return null; + } + + @Override + public void onMetaViewportFitChange(final GeckoSession session, final String viewportFit) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return; + } + WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); + if (viewportFit.equals("cover")) { + layoutParams.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } else if (viewportFit.equals("contain")) { + layoutParams.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + } else { + layoutParams.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; + } + getWindow().setAttributes(layoutParams); + } + + @Override + public void onShowDynamicToolbar(final GeckoSession session) { + final View toolbar = findViewById(R.id.toolbar); + if (toolbar != null) { + toolbar.setTranslationY(0f); + } + } + + @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<Integer> 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<ContentPermission> 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<ContentPermission> 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<AllowOrDeny> onLoadRequest( + final GeckoSession session, final LoadRequest request) { + Log.d( + LOGTAG, + "onLoadRequest=" + + request.uri + + " triggerUri=" + + request.triggerUri + + " where=" + + request.target + + " isRedirect=" + + request.isRedirect + + " isDirectNavigation=" + + request.isDirectNavigation); + + return GeckoResult.allow(); + } + + @Override + public GeckoResult<AllowOrDeny> onSubframeLoadRequest( + final GeckoSession session, final LoadRequest request) { + Log.d( + LOGTAG, + "onSubframeLoadRequest=" + + request.uri + + " triggerUri=" + + request.triggerUri + + " isRedirect=" + + request.isRedirect + + "isDirectNavigation=" + + request.isDirectNavigation); + + return GeckoResult.allow(); + } + + @Override + public GeckoResult<GeckoSession> onNewSession(final GeckoSession session, final String uri) { + final TabSession newSession = createSession(); + mToolbarView.updateTabCount(); + setGeckoViewSession(newSession); + // A reference to newSession is stored by mTabSessionManager, + // which prevents the session from being garbage-collected. + return GeckoResult.fromValue(newSession); + } + + private String categoryToString(final int category) { + switch (category) { + case WebRequestError.ERROR_CATEGORY_UNKNOWN: + return "ERROR_CATEGORY_UNKNOWN"; + case WebRequestError.ERROR_CATEGORY_SECURITY: + return "ERROR_CATEGORY_SECURITY"; + case WebRequestError.ERROR_CATEGORY_NETWORK: + return "ERROR_CATEGORY_NETWORK"; + case WebRequestError.ERROR_CATEGORY_CONTENT: + return "ERROR_CATEGORY_CONTENT"; + case WebRequestError.ERROR_CATEGORY_URI: + return "ERROR_CATEGORY_URI"; + case WebRequestError.ERROR_CATEGORY_PROXY: + return "ERROR_CATEGORY_PROXY"; + case WebRequestError.ERROR_CATEGORY_SAFEBROWSING: + return "ERROR_CATEGORY_SAFEBROWSING"; + default: + return "UNKNOWN"; + } + } + + private String errorToString(final int error) { + switch (error) { + case WebRequestError.ERROR_UNKNOWN: + return "ERROR_UNKNOWN"; + case WebRequestError.ERROR_SECURITY_SSL: + return "ERROR_SECURITY_SSL"; + case WebRequestError.ERROR_SECURITY_BAD_CERT: + return "ERROR_SECURITY_BAD_CERT"; + case WebRequestError.ERROR_NET_RESET: + return "ERROR_NET_RESET"; + case WebRequestError.ERROR_NET_INTERRUPT: + return "ERROR_NET_INTERRUPT"; + case WebRequestError.ERROR_NET_TIMEOUT: + return "ERROR_NET_TIMEOUT"; + case WebRequestError.ERROR_CONNECTION_REFUSED: + return "ERROR_CONNECTION_REFUSED"; + case WebRequestError.ERROR_UNKNOWN_PROTOCOL: + return "ERROR_UNKNOWN_PROTOCOL"; + case WebRequestError.ERROR_UNKNOWN_HOST: + return "ERROR_UNKNOWN_HOST"; + case WebRequestError.ERROR_UNKNOWN_SOCKET_TYPE: + return "ERROR_UNKNOWN_SOCKET_TYPE"; + case WebRequestError.ERROR_UNKNOWN_PROXY_HOST: + return "ERROR_UNKNOWN_PROXY_HOST"; + case WebRequestError.ERROR_MALFORMED_URI: + return "ERROR_MALFORMED_URI"; + case WebRequestError.ERROR_REDIRECT_LOOP: + return "ERROR_REDIRECT_LOOP"; + case WebRequestError.ERROR_SAFEBROWSING_PHISHING_URI: + return "ERROR_SAFEBROWSING_PHISHING_URI"; + case WebRequestError.ERROR_SAFEBROWSING_MALWARE_URI: + return "ERROR_SAFEBROWSING_MALWARE_URI"; + case WebRequestError.ERROR_SAFEBROWSING_UNWANTED_URI: + return "ERROR_SAFEBROWSING_UNWANTED_URI"; + case WebRequestError.ERROR_SAFEBROWSING_HARMFUL_URI: + return "ERROR_SAFEBROWSING_HARMFUL_URI"; + case WebRequestError.ERROR_CONTENT_CRASHED: + return "ERROR_CONTENT_CRASHED"; + case WebRequestError.ERROR_OFFLINE: + return "ERROR_OFFLINE"; + case WebRequestError.ERROR_PORT_BLOCKED: + return "ERROR_PORT_BLOCKED"; + case WebRequestError.ERROR_PROXY_CONNECTION_REFUSED: + return "ERROR_PROXY_CONNECTION_REFUSED"; + case WebRequestError.ERROR_FILE_NOT_FOUND: + return "ERROR_FILE_NOT_FOUND"; + case WebRequestError.ERROR_FILE_ACCESS_DENIED: + return "ERROR_FILE_ACCESS_DENIED"; + case WebRequestError.ERROR_INVALID_CONTENT_ENCODING: + return "ERROR_INVALID_CONTENT_ENCODING"; + case WebRequestError.ERROR_UNSAFE_CONTENT_TYPE: + return "ERROR_UNSAFE_CONTENT_TYPE"; + case WebRequestError.ERROR_CORRUPTED_CONTENT: + return "ERROR_CORRUPTED_CONTENT"; + 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<String> onLoadError( + final GeckoSession session, final String uri, final WebRequestError error) { + Log.d( + LOGTAG, + "onLoadError=" + uri + " error category=" + error.category + " error=" + error.code); + + return GeckoResult.fromValue("data:text/html," + createErrorPage(error.category, error.code)); + } + } + + private class ExampleContentBlockingDelegate implements ContentBlocking.Delegate { + private int mBlockedAds = 0; + private int mBlockedAnalytics = 0; + private int mBlockedSocial = 0; + private int mBlockedContent = 0; + private int mBlockedTest = 0; + private int mBlockedStp = 0; + + private void clearCounters() { + mBlockedAds = 0; + mBlockedAnalytics = 0; + mBlockedSocial = 0; + mBlockedContent = 0; + mBlockedTest = 0; + mBlockedStp = 0; + } + + private void logCounters() { + Log.d( + LOGTAG, + "Trackers blocked: " + + mBlockedAds + + " ads, " + + mBlockedAnalytics + + " analytics, " + + mBlockedSocial + + " social, " + + mBlockedContent + + " content, " + + mBlockedTest + + " test, " + + mBlockedStp + + "stp"); + } + + @Override + public void onContentBlocked( + final GeckoSession session, final ContentBlocking.BlockEvent event) { + Log.d( + LOGTAG, + "onContentBlocked" + + " AT: " + + event.getAntiTrackingCategory() + + " SB: " + + event.getSafeBrowsingCategory() + + " CB: " + + event.getCookieBehaviorCategory() + + " URI: " + + event.uri); + if ((event.getAntiTrackingCategory() & ContentBlocking.AntiTracking.TEST) != 0) { + mBlockedTest++; + } + if ((event.getAntiTrackingCategory() & ContentBlocking.AntiTracking.AD) != 0) { + mBlockedAds++; + } + if ((event.getAntiTrackingCategory() & ContentBlocking.AntiTracking.ANALYTIC) != 0) { + mBlockedAnalytics++; + } + if ((event.getAntiTrackingCategory() & ContentBlocking.AntiTracking.SOCIAL) != 0) { + mBlockedSocial++; + } + if ((event.getAntiTrackingCategory() & ContentBlocking.AntiTracking.CONTENT) != 0) { + mBlockedContent++; + } + if ((event.getAntiTrackingCategory() & ContentBlocking.AntiTracking.STP) != 0) { + mBlockedStp++; + } + } + + @Override + public void onContentLoaded( + final GeckoSession session, final ContentBlocking.BlockEvent event) { + Log.d( + LOGTAG, + "onContentLoaded" + + " AT: " + + event.getAntiTrackingCategory() + + " SB: " + + event.getSafeBrowsingCategory() + + " CB: " + + event.getCookieBehaviorCategory() + + " URI: " + + event.uri); + } + } + + private class ExampleMediaDelegate implements GeckoSession.MediaDelegate { + private Integer mLastNotificationId = 100; + private Integer mNotificationId; + 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 final class ExampleTelemetryDelegate implements RuntimeTelemetry.Delegate { + @Override + public void onHistogram(final @NonNull RuntimeTelemetry.Histogram histogram) { + Log.d(LOGTAG, "onHistogram " + histogram); + } + + @Override + public void onBooleanScalar(final @NonNull RuntimeTelemetry.Metric<Boolean> scalar) { + Log.d(LOGTAG, "onBooleanScalar " + scalar); + } + + @Override + public void onLongScalar(final @NonNull RuntimeTelemetry.Metric<Long> scalar) { + Log.d(LOGTAG, "onLongScalar " + scalar); + } + + @Override + public void onStringScalar(final @NonNull RuntimeTelemetry.Metric<String> scalar) { + Log.d(LOGTAG, "onStringScalar " + scalar); + } + } +} diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewBottomBehavior.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewBottomBehavior.java new file mode 100644 index 0000000000..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<GeckoView> { + public GeckoViewBottomBehavior(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, GeckoView child, View dependency) { + return dependency instanceof Toolbar; + } + + @Override + public boolean onDependentViewChanged( + CoordinatorLayout parent, GeckoView child, View dependency) { + child.setVerticalClipping(Math.round(-dependency.getTranslationY())); + return true; + } +} diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/LocationView.java new file mode 100644 index 0000000000..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). + * + * <p>This code is a simplified version of the NestedScrollView implementation which can be found in + * the support library: [android.support.v4.widget.NestedScrollView] + * + * <p>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. + * + * <p>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<TabSession> mTabSessions = new ArrayList<>(); + private int mCurrentSessionIndex = 0; + private TabObserver mTabObserver; + private boolean mTrackingProtection; + + public interface TabObserver { + void onCurrentSession(TabSession session); + } + + public TabSessionManager() {} + + public void unregisterWebExtension() { + for (final TabSession session : mTabSessions) { + session.action = null; + } + } + + public void setWebExtensionDelegates( + WebExtension extension, + WebExtension.ActionDelegate actionDelegate, + WebExtension.SessionTabDelegate tabDelegate) { + for (final TabSession session : mTabSessions) { + final WebExtension.SessionController sessionController = session.getWebExtensionController(); + sessionController.setActionDelegate(extension, actionDelegate); + sessionController.setTabDelegate(extension, tabDelegate); + } + } + + public void setUseTrackingProtection(boolean trackingProtection) { + if (trackingProtection == mTrackingProtection) { + return; + } + mTrackingProtection = trackingProtection; + + for (final TabSession session : mTabSessions) { + session.getSettings().setUseTrackingProtection(trackingProtection); + } + } + + public void setTabObserver(TabObserver observer) { + mTabObserver = observer; + } + + public void addSession(TabSession session) { + mTabSessions.add(session); + } + + public TabSession getSession(int index) { + if (index >= mTabSessions.size() || 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<TabSession> getSessions() { + return mTabSessions; + } +} diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarBottomBehavior.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarBottomBehavior.java new file mode 100644 index 0000000000..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<View> { + public ToolbarBottomBehavior(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + @Override + public boolean onStartNestedScroll( + CoordinatorLayout coordinatorLayout, + View child, + View directTargetChild, + View target, + int axes, + int type) { + NestedGeckoView geckoView = (NestedGeckoView) target; + if (axes == ViewCompat.SCROLL_AXIS_VERTICAL + && geckoView.getInputResult() == PanZoomController.INPUT_RESULT_HANDLED) { + return true; + } + + if (geckoView.getInputResult() == PanZoomController.INPUT_RESULT_UNHANDLED) { + // Restore the toolbar to the original (visible) state, this is what A-C does. + child.setTranslationY(0f); + } + + return false; + } + + @Override + public void onStopNestedScroll( + CoordinatorLayout coordinatorLayout, View child, View target, int type) { + // Snap up or down the user stops scrolling. + if (child.getTranslationY() >= (child.getHeight() / 2f)) { + child.setTranslationY(child.getHeight()); + } else { + child.setTranslationY(0f); + } + } + + @Override + public void onNestedPreScroll( + CoordinatorLayout coordinatorLayout, + View child, + View target, + int dx, + int dy, + int[] consumed, + int type) { + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); + child.setTranslationY(Math.max(0f, Math.min(child.getHeight(), child.getTranslationY() + dy))); + } +} diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java new file mode 100644 index 0000000000..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 Binary files differnew file mode 100644 index 0000000000..6c7b806fa9 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_camera.png 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 Binary files differnew file mode 100644 index 0000000000..f56254888b --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic.png 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 Binary files differnew file mode 100644 index 0000000000..091ec077dd --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/alert_mic_camera.png 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 Binary files differnew file mode 100644 index 0000000000..bb0a241e33 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_crash.png 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 Binary files differnew file mode 100644 index 0000000000..374ef69857 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-hdpi/ic_status_logo.png 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 Binary files differnew file mode 100644 index 0000000000..d385b3e5cc --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-mdpi/ic_crash.png 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 Binary files differnew file mode 100644 index 0000000000..efe4811071 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_camera.png 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 Binary files differnew file mode 100644 index 0000000000..198a7ba318 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic.png 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 Binary files differnew file mode 100644 index 0000000000..26ba6520b9 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/alert_mic_camera.png 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 Binary files differnew file mode 100644 index 0000000000..3d9bd68082 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xhdpi/ic_crash.png 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 Binary files differnew file mode 100644 index 0000000000..5c21f5bd4f --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_camera.png 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 Binary files differnew file mode 100644 index 0000000000..561816f087 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic.png 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 Binary files differnew file mode 100644 index 0000000000..3b34d229ed --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/alert_mic_camera.png 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 Binary files differnew file mode 100644 index 0000000000..39d5c7f57c --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/ic_crash.png 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 Binary files differnew file mode 100644 index 0000000000..162891e993 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable-xxhdpi/logo.png diff --git a/mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml b/mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml new file mode 100644 index 0000000000..9cfaf557e2 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid + android:id="@+id/browser_action_badge_background" + android:color="#176d7a" + /> + <corners android:radius="5dp" /> +</shape>
\ No newline at end of file diff --git a/mobile/android/geckoview_example/src/main/res/drawable/tab_number_background.xml b/mobile/android/geckoview_example/src/main/res/drawable/tab_number_background.xml new file mode 100644 index 0000000000..d5292fda8f --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/drawable/tab_number_background.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + + <solid android:color="@android:color/transparent" /> + + <corners + android:bottomRightRadius="2dp" + android:bottomLeftRadius="2dp" + android:topLeftRadius="2dp" + android:topRightRadius="2dp"/> + + <stroke + android:width="1dp" + android:color="@android:color/darker_gray" /> + +</shape>
\ No newline at end of file diff --git a/mobile/android/geckoview_example/src/main/res/layout/activity_settings.xml b/mobile/android/geckoview_example/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000000..d6c4025ea6 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/layout/activity_settings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/colorBackgroundDark" + app:theme="@style/ToolBarStyle" + android:elevation="4dp"/> + + <FrameLayout + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:background="@color/colorPrimaryDark"/> + +</LinearLayout> diff --git a/mobile/android/geckoview_example/src/main/res/layout/browser_action.xml b/mobile/android/geckoview_example/src/main/res/layout/browser_action.xml new file mode 100644 index 0000000000..fa06e95e88 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/layout/browser_action.xml @@ -0,0 +1,32 @@ +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="?android:actionBarSize" + android:layout_height="?android:actionBarSize" + android:gravity="center" + android:orientation="vertical"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + > + <ImageView + android:id="@+id/browser_action_icon" + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_centerInParent="true" + /> + </RelativeLayout> + + <TextView + android:id="@+id/browser_action_badge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/rounded_bg" + android:textColor="@color/colorPrimaryDark" + android:layout_alignParentRight="true" + android:paddingLeft="3dp" + android:paddingRight="3dp" + android:layout_marginTop="3dp" + android:layout_marginRight="3dp" + android:text="12" + /> +</RelativeLayout> diff --git a/mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml b/mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml new file mode 100644 index 0000000000..a1bb627d17 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml @@ -0,0 +1,13 @@ +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <org.mozilla.geckoview.GeckoView + android:id="@+id/gecko_view_popup" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="none" + /> +</RelativeLayout> diff --git a/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml new file mode 100644 index 0000000000..8ee4a615a9 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml @@ -0,0 +1,32 @@ +<androidx.coordinatorlayout.widget.CoordinatorLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/main" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <org.mozilla.geckoview_example.NestedGeckoView + android:id="@+id/gecko_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="none" + app:layout_behavior="org.mozilla.geckoview_example.GeckoViewBottomBehavior" + /> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?android:actionBarSize" + android:layout_gravity="bottom" + android:background="#eeeeee" + app:layout_behavior="org.mozilla.geckoview_example.ToolbarBottomBehavior" + app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed" /> + + <ProgressBar + android:id="@+id/page_progress" + style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal" + android:layout_width="match_parent" + android:layout_height="3dp" + android:layout_alignTop="@id/gecko_view" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/mobile/android/geckoview_example/src/main/res/menu/actions.xml b/mobile/android/geckoview_example/src/main/res/menu/actions.xml new file mode 100644 index 0000000000..d738fa42da --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/menu/actions.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + <item android:title="@string/tracking_protection_ex" android:checkable="true" + android:id="@+id/action_tpe" app:showAsAction="never" /> + <item android:title="@string/desktop_mode" android:id="@+id/desktop_mode" android:checkable="true" + app:showAsAction="never" /> + <item android:title="@string/collapse" android:id="@+id/collapse" android:checkable="true" + app:showAsAction="never" /> + <item android:title="@string/private_browsing" android:checkable="true" android:id="@+id/action_pb"/> + <item android:title="@string/new_tab" android:id="@+id/action_new_tab"/> + <item android:title="@string/install_addon" android:id="@+id/install_addon"/> + <item android:title="@string/update_addon" android:id="@+id/update_addon"/> + <item android:title="@string/close_tab" android:id="@+id/action_close_tab"/> + <item android:title="@string/forward" android:id="@+id/action_forward"/> + <item android:title="@string/reload" android:id="@+id/action_reload"/> + <item android:title="@string/save_pdf" android:id="@+id/save_pdf"/> + <item android:title="@string/settings" android:id="@+id/settings" app:showAsAction="never" /> +</menu> diff --git a/mobile/android/geckoview_example/src/main/res/values/colors.xml b/mobile/android/geckoview_example/src/main/res/values/colors.xml new file mode 100644 index 0000000000..157834fe0c --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="colorPrimary">#3F51B5</color> + <color name="colorBackgroundDark">#00a996</color> + <color name="colorPrimaryDark">#FFFFFF</color> + <color name="colorAccent">#FF4081</color> +</resources> diff --git a/mobile/android/geckoview_example/src/main/res/values/ids.xml b/mobile/android/geckoview_example/src/main/res/values/ids.xml new file mode 100644 index 0000000000..068f6e3d95 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/values/ids.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item name="toolbar_layout" type="id"/> + <item name="url_bar" type="id"/> + <item name="browser_action" type="id"/> + <item name="tabs_button" type="id"/> +</resources> diff --git a/mobile/android/geckoview_example/src/main/res/values/strings.xml b/mobile/android/geckoview_example/src/main/res/values/strings.xml new file mode 100644 index 0000000000..a71bc46541 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml @@ -0,0 +1,151 @@ +<resources> + <string name="app_name">GeckoView Example</string> + <string name="activity_label">GeckoView Example</string> + <string name="location_hint">Enter URL or search keywords...</string> + <string name="username">Username</string> + <string name="password">Password</string> + <string name="clear_field">Clear</string> + <string name="request_storage">Allow access to device storage for "%1$s"?</string> + <string name="request_storage_access">Allow third parties to access first party storage for "%1$s"?</string> + <string name="request_geolocation">Share location with "%1$s"?</string> + <string name="request_notification">Allow notifications for "%1$s"?</string> + <string name="request_video">Share video with "%1$s"</string> + <string name="request_audio">Share audio with "%1$s"</string> + <string name="request_media">Share video and audio with "%1$s"</string> + <string name="request_xr">Share WebXR displays with "%1$s"?</string> + <string name="request_autoplay">Allow video to autoplay on "%1$s"?</string> + <string name="request_media_key_system_access">Allow system media key access for "%1$s"?</string> + <string name="media_back_camera">Back camera</string> + <string name="media_front_camera">Front camera</string> + <string name="media_microphone">Microphone</string> + <string name="media_other">Unknown source</string> + + <string name="crash_native">Native</string> + <string name="crash_java">Java</string> + <string name="crash_content_native">Content (Native)</string> + <string name="tracking_protection">Tracking Protection</string> + <string name="tracking_protection_ex">TP exception</string> + <string name="private_browsing">Private Browsing</string> + <string name="remote_debugging">Remote Debugging</string> + <string name="forward">Forward</string> + <string name="reload">Reload</string> + <string name="settings">Settings...</string> + <string name="crashed_title">GeckoView Example Crashed</string> + <string name="crashed_text">Tap to report to Mozilla.</string> + <string name="crashed_ignore">Ignore</string> + <string name="crashed_report">Report</string> + <string name="device_sharing_microphone">Microphone is on</string> + <string name="device_sharing_camera">Camera is on</string> + <string name="device_sharing_camera_and_mic">Camera and microphone are on</string> + <string name="new_tab">New tab</string> + <string name="install_addon">Install Add-on...</string> + <string name="install_addon_hint">Add-on URL: https://addons.mozilla.org/...</string> + <string name="update_addon">Update Add-on</string> + <string name="close_tab">Close tab</string> + <string name="desktop_mode">Desktop site</string> + <string name="collapse">Collapse GV</string> + <string name="slow_script">A script on this page is causing your web browser to run slowly</string> + <string name="wait">Wait</string> + <string name="stop">Stop</string> + <string name="install">Install</string> + <string name="cancel">Cancel</string> + <string name="addon_uri">WebExtension xpi URL</string> + <string name="save_pdf">Save as PDF</string> + + # Preferences + <string name="key_tracking_protection">tracking_protection</string> + <item type="bool" name="tracking_protection_default">true</item> + + <string name="key_javascript_enabled">javascript_enabled</string> + <item type="bool" name="javascript_enabled_default">true</item> + + <string name="key_remote_debugging">remote_debugging</string> + <item type="bool" name="remote_debugging_default">true</item> + + <string name="key_dfpi">dfpi</string> + <item type="bool" name="dfpi_default">false</item> + + <string name="key_autoplay">autoplay</string> + <item type="bool" name="autoplay_default">false</item> + + <string name="key_allow_extensions_in_private_browsing">allow_extensions_in_private_browsing</string> + <item type="bool" name="allow_extensions_in_private_browsing_default">false</item> + + <string name="key_user_agent_override">user_agent_override</string> + <string name="user_agent_override_default"></string> + <string-array name="user_agent_override_display_names"> + <item>Default</item> + <item>Chrome 80 on Android</item> + <item>Safari 12 on iPhone</item> + </string-array> + <string-array name="user_agent_override_values"> + <item></item> + <item>Mozilla/5.0 (Linux; Android 10; Z832 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Mobile Safari/537.36</item> + <item>Mozilla/5.0 (iPhone; CPU OS 10_15_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/14E304 Safari/605.1.15</item> + </string-array> + + <string name="key_cookie_banner_handling">cookie_banner_handling</string> + <string name="key_cookie_banner_handling_pb">cookie_banner_handling_pb</string> + <string name="cookie_banner_handling_default">disabled</string> + <string name="cookie_banner_handling_pb_default">reject_accept_all</string> + <string name="key_enhanced_tracking_protection">enhanced_tracking_protection</string> + <string name="enhanced_tracking_protection_default">standard</string> + <string-array name="enhanced_tracking_protection_display_names"> + <item>Disabled</item> + <item>Enabled (standard)</item> + <item>Enabled (strict)</item> + </string-array> + <string-array name="enhanced_tracking_protection_values"> + <item>disabled</item> + <item>standard</item> + <item>strict</item> + </string-array> + <string-array name="cookie_banner_handling_names"> + <item>Disabled</item> + <item>Enabled (reject all)</item> + <item>Enabled (reject or accept all)</item> + </string-array> + <string-array name="cookie_banner_handling_values"> + <item>disabled</item> + <item>reject_all</item> + <item>reject_accept_all</item> + </string-array> + + <string name="key_preferred_color_scheme">preferred_color_scheme</string> + <item type="integer" name="preferred_color_scheme_default">-1</item> + <string-array name="pref_preferred_color_scheme_display_names"> + <item>Follow System Preference</item> + <item>Light</item> + <item>Dark</item> + </string-array> + <string-array name="pref_preferred_color_scheme_values"> + <item>-1</item> + <item>0</item> + <item>1</item> + </string-array> + + <string name="key_display_mode">display_mode</string> + <item type="integer" name="display_mode_default">0</item> + <string-array name="pref_display_mode_names"> + <item>Browser</item> + <item>MinimalUi</item> + <item>Standalone</item> + <item>Fullscreen</item> + </string-array> + <string-array name="pref_display_mode_values"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </string-array> + + <string name="before_unload_message">This page is asking you to confirm that you want to leave - data you have entered may not be saved</string> + <string name="before_unload_title">Are you sure?</string> + <string name="before_unload_leave_page">Leave Page</string> + <string name="before_unload_stay">Stay on Page</string> + + <string name="repost_confirm_message">To display this page, GeckoViewExample must send information that will repeat any action (such as a search or order confirmation) that was performed earlier.</string> + <string name="repost_confirm_title">Are you sure?</string> + <string name="repost_confirm_resend">Resend</string> + <string name="repost_confirm_cancel">Cancel</string> +</resources> diff --git a/mobile/android/geckoview_example/src/main/res/values/styles.xml b/mobile/android/geckoview_example/src/main/res/values/styles.xml new file mode 100644 index 0000000000..e8420145ba --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/values/styles.xml @@ -0,0 +1,7 @@ +<resources> + <style name="ToolBarStyle" parent="Theme.AppCompat"> + <item name="android:textColorPrimary">@android:color/white</item> + <item name="android:textColorSecondary">@android:color/white</item> + <item name="actionMenuTextColor">@android:color/white</item> + </style> +</resources> diff --git a/mobile/android/geckoview_example/src/main/res/xml/settings.xml b/mobile/android/geckoview_example/src/main/res/xml/settings.xml new file mode 100644 index 0000000000..a29be429a0 --- /dev/null +++ b/mobile/android/geckoview_example/src/main/res/xml/settings.xml @@ -0,0 +1,71 @@ +<PreferenceScreen + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <SwitchPreferenceCompat + app:key="@string/key_tracking_protection" + app:title="Enable Tracking Protection" + app:defaultValue="@bool/tracking_protection_default"/> + <ListPreference + app:key="@string/key_enhanced_tracking_protection" + app:title="Enhanced Tracking Protection" + app:summary="%s" + app:entries="@array/enhanced_tracking_protection_display_names" + app:entryValues="@array/enhanced_tracking_protection_values" + app:defaultValue="@string/enhanced_tracking_protection_default"/> + <ListPreference + app:key="@string/key_cookie_banner_handling" + app:title="Cookie Banner Handling" + app:summary="%s" + app:entries="@array/cookie_banner_handling_names" + app:entryValues="@array/cookie_banner_handling_values" + app:defaultValue="@string/cookie_banner_handling_default"/> + <ListPreference + app:key="@string/key_cookie_banner_handling_pb" + app:title="Cookie Banner Handling Private mode" + app:summary="%s" + app:entries="@array/cookie_banner_handling_names" + app:entryValues="@array/cookie_banner_handling_values" + app:defaultValue="@string/cookie_banner_handling_pb_default"/> + <SwitchPreferenceCompat + app:key="@string/key_dfpi" + app:title="Enable Dynamic FPI" + app:defaultValue="@bool/dfpi_default"/> + <SwitchPreferenceCompat + app:key="@string/key_autoplay" + app:title="Allow Autoplay" + app:defaultValue="@bool/autoplay_default"/> + <SwitchPreferenceCompat + app:key="@string/key_remote_debugging" + app:title="Remote Debugging" + app:defaultValue="@bool/remote_debugging_default"/> + <ListPreference + app:key="@string/key_preferred_color_scheme" + app:title="Preferred Color Scheme" + app:summary="%s" + app:entries="@array/pref_preferred_color_scheme_display_names" + app:entryValues="@array/pref_preferred_color_scheme_values" + app:defaultValue="@integer/preferred_color_scheme_default"/> + <ListPreference + app:key="@string/key_user_agent_override" + app:title="User Agent String Override" + app:summary="%s" + app:entries="@array/user_agent_override_display_names" + app:entryValues="@array/user_agent_override_values" + app:defaultValue="@string/user_agent_override_default"/> + <SwitchPreferenceCompat + app:key="@string/key_allow_extensions_in_private_browsing" + app:title="Run extensions in private tabs" + app:defaultValue="@bool/allow_extensions_in_private_browsing_default"/> + <ListPreference + app:key="@string/key_display_mode" + app:title="Display Mode" + app:summary="%s" + app:entries="@array/pref_display_mode_names" + app:entryValues="@array/pref_display_mode_values" + app:defaultValue="@integer/display_mode_default"/> + <SwitchPreferenceCompat + app:key="@string/key_javascript_enabled" + app:title="Javascript Enabled" + app:defaultValue="@bool/javascript_enabled_default"/> + +</PreferenceScreen> |