/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.geckoview_example;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.SystemClock;
import android.text.InputType;
import android.util.Log;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONObject;
import org.mozilla.geckoview.AllowOrDeny;
import org.mozilla.geckoview.Autocomplete;
import org.mozilla.geckoview.BasicSelectionActionDelegate;
import org.mozilla.geckoview.ContentBlocking;
import org.mozilla.geckoview.GeckoResult;
import org.mozilla.geckoview.GeckoRuntime;
import org.mozilla.geckoview.GeckoRuntimeSettings;
import org.mozilla.geckoview.GeckoSession;
import org.mozilla.geckoview.GeckoSession.PermissionDelegate;
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission;
import org.mozilla.geckoview.GeckoSessionSettings;
import org.mozilla.geckoview.GeckoView;
import org.mozilla.geckoview.GeckoWebExecutor;
import org.mozilla.geckoview.Image;
import org.mozilla.geckoview.MediaSession;
import org.mozilla.geckoview.OrientationController;
import org.mozilla.geckoview.RuntimeTelemetry;
import org.mozilla.geckoview.SlowScriptResponse;
import org.mozilla.geckoview.WebExtension;
import org.mozilla.geckoview.WebExtensionController;
import org.mozilla.geckoview.WebNotification;
import org.mozilla.geckoview.WebNotificationDelegate;
import org.mozilla.geckoview.WebRequest;
import org.mozilla.geckoview.WebRequestError;
import org.mozilla.geckoview.WebResponse;

interface WebExtensionDelegate {
  default GeckoSession toggleBrowserActionPopup(boolean force) {
    return null;
  }

  default void onActionButton(ActionButton button) {}

  default TabSession getSession(GeckoSession session) {
    return null;
  }

  default TabSession getCurrentSession() {
    return null;
  }

  default void closeTab(TabSession session) {}

  default void updateTab(TabSession session, WebExtension.UpdateTabDetails details) {}

  default TabSession openNewTab(WebExtension.CreateTabDetails details) {
    return null;
  }
}

class WebExtensionManager
    implements WebExtension.ActionDelegate,
        WebExtension.SessionTabDelegate,
        WebExtension.TabDelegate,
        WebExtensionController.PromptDelegate,
        WebExtensionController.DebuggerDelegate,
        TabSessionManager.TabObserver {
  public WebExtension extension;

  private LruCache<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);
    mGeckoView.setActivityContextDelegate(new ExampleActivityDelegate());
    mTabSessionManager = new TabSessionManager();

    setSupportActionBar(findViewById(R.id.toolbar));

    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
    preferences.registerOnSharedPreferenceChangeListener(this);
    // Read initial preference state
    onPreferencesChange(preferences);

    mToolbarView = new ToolbarLayout(this, mTabSessionManager);
    mToolbarView.setId(R.id.toolbar_layout);
    mToolbarView.setTabListener(this);

    getSupportActionBar()
        .setCustomView(
            mToolbarView,
            new ActionBar.LayoutParams(
                ActionBar.LayoutParams.MATCH_PARENT, ActionBar.LayoutParams.WRAP_CONTENT));
    getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);

    mFullAccessibilityTree = getIntent().getBooleanExtra(FULL_ACCESSIBILITY_TREE_EXTRA, false);
    mProgressView = findViewById(R.id.page_progress);

    if (sGeckoRuntime == null) {
      final GeckoRuntimeSettings.Builder runtimeSettingsBuilder =
          new GeckoRuntimeSettings.Builder();

      if (BuildConfig.DEBUG) {
        // In debug builds, we want to load JavaScript resources fresh with
        // each build.
        runtimeSettingsBuilder.arguments(new String[] {"-purgecaches"});
      }

      final Bundle extras = getIntent().getExtras();
      if (extras != null) {
        runtimeSettingsBuilder.extras(extras);
      }
      runtimeSettingsBuilder
          .remoteDebuggingEnabled(mRemoteDebugging.value())
          .consoleOutput(true)
          .contentBlocking(
              new ContentBlocking.Settings.Builder()
                  .antiTracking(
                      ContentBlocking.AntiTracking.DEFAULT | ContentBlocking.AntiTracking.STP)
                  .safeBrowsing(ContentBlocking.SafeBrowsing.DEFAULT)
                  .cookieBehavior(ContentBlocking.CookieBehavior.ACCEPT_NON_TRACKERS)
                  .cookieBehaviorPrivateMode(ContentBlocking.CookieBehavior.ACCEPT_NON_TRACKERS)
                  .enhancedTrackingProtectionLevel(ContentBlocking.EtpLevel.DEFAULT)
                  .build())
          .crashHandler(ExampleCrashHandler.class)
          .preferredColorScheme(mPreferredColorScheme.value())
          .telemetryDelegate(new ExampleTelemetryDelegate())
          .javaScriptEnabled(mJavascriptEnabled.value())
          .aboutConfigEnabled(true);

      sGeckoRuntime = GeckoRuntime.create(this, runtimeSettingsBuilder.build());

      sExtensionManager = new WebExtensionManager(sGeckoRuntime, mTabSessionManager);
      mTabSessionManager.setTabObserver(sExtensionManager);

      sGeckoRuntime.getWebExtensionController().setDebuggerDelegate(sExtensionManager);
      sGeckoRuntime.setAutocompleteStorageDelegate(new ExampleAutocompleteStorageDelegate());
      sGeckoRuntime.getOrientationController().setDelegate(new ExampleOrientationDelegate());
      sGeckoRuntime.setServiceWorkerDelegate(
          new GeckoRuntime.ServiceWorkerDelegate() {
            @NonNull
            @Override
            public GeckoResult<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.setMediaSessionDelegate(new ExampleMediaSessionDelegate(this));

    session.setSelectionActionDelegate(new BasicSelectionActionDelegate(this));
    if (sExtensionManager.extension != null) {
      final WebExtension.SessionController sessionController = session.getWebExtensionController();
      sessionController.setActionDelegate(sExtensionManager.extension, sExtensionManager);
      sessionController.setTabDelegate(sExtensionManager.extension, sExtensionManager);
    }

    updateDesktopMode(session);
  }

  private void recreateSession() {
    recreateSession(mTabSessionManager.getCurrentSession());
  }

  private void recreateSession(TabSession session) {
    if (session != null) {
      mTabSessionManager.closeSession(session);
    }

    session = createSession();
    session.open(sGeckoRuntime);
    mTabSessionManager.setCurrentSession(session);
    mGeckoView.setSession(session);
    sGeckoRuntime.getWebExtensionController().setTabActive(session, true);
    if (mCurrentUri != null) {
      session.loadUri(mCurrentUri);
    }
  }

  @Override
  public void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    if (savedInstanceState != null && mGeckoView.getSession() != null) {
      mTabSessionManager.setCurrentSession((TabSession) mGeckoView.getSession());
      sGeckoRuntime.getWebExtensionController().setTabActive(mGeckoView.getSession(), true);
    } else {
      recreateSession();
    }
  }

  private void updateDesktopMode(GeckoSession session) {
    session
        .getSettings()
        .setViewportMode(
            mDesktopMode
                ? GeckoSessionSettings.VIEWPORT_MODE_DESKTOP
                : GeckoSessionSettings.VIEWPORT_MODE_MOBILE);
    session
        .getSettings()
        .setUserAgentMode(
            mDesktopMode
                ? GeckoSessionSettings.USER_AGENT_MODE_DESKTOP
                : GeckoSessionSettings.USER_AGENT_MODE_MOBILE);
  }

  @Override
  public void onBackPressed() {
    GeckoSession session = mTabSessionManager.getCurrentSession();
    if (mFullScreen && session != null) {
      session.exitFullScreen();
      return;
    }

    if (mCanGoBack && session != null) {
      session.goBack();
      return;
    }

    super.onBackPressed();
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.actions, menu);
    return true;
  }

  @Override
  public boolean onPrepareOptionsMenu(Menu menu) {
    menu.findItem(R.id.action_pb).setChecked(mUsePrivateBrowsing);
    menu.findItem(R.id.collapse).setChecked(mCollapsed);
    menu.findItem(R.id.desktop_mode).setChecked(mDesktopMode);
    menu.findItem(R.id.action_tpe)
        .setChecked(
            mTrackingProtectionPermission != null
                && mTrackingProtectionPermission.value == ContentPermission.VALUE_ALLOW);
    menu.findItem(R.id.action_forward).setEnabled(mCanGoForward);

    final boolean hasSession = mTabSessionManager.getCurrentSession() != null;
    menu.findItem(R.id.action_reload).setEnabled(hasSession);
    menu.findItem(R.id.action_forward).setEnabled(hasSession);
    menu.findItem(R.id.action_close_tab).setEnabled(hasSession);
    menu.findItem(R.id.action_tpe).setEnabled(hasSession && mTrackingProtectionPermission != null);
    menu.findItem(R.id.action_pb).setEnabled(hasSession);
    menu.findItem(R.id.desktop_mode).setEnabled(hasSession);

    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    GeckoSession session = mTabSessionManager.getCurrentSession();
    switch (item.getItemId()) {
      case R.id.action_reload:
        session.reload();
        break;
      case R.id.action_forward:
        session.goForward();
        break;
      case R.id.action_tpe:
        sGeckoRuntime
            .getStorageController()
            .setPermission(
                mTrackingProtectionPermission,
                mTrackingProtectionPermission.value == ContentPermission.VALUE_ALLOW
                    ? ContentPermission.VALUE_DENY
                    : ContentPermission.VALUE_ALLOW);
        session.reload();
        break;
      case R.id.desktop_mode:
        mDesktopMode = !mDesktopMode;
        updateDesktopMode(session);
        session.reload();
        break;
      case R.id.action_pb:
        mUsePrivateBrowsing = !mUsePrivateBrowsing;
        recreateSession();
        break;
      case R.id.collapse:
        mCollapsed = !mCollapsed;
        setViewVisibility(mGeckoView, !mCollapsed);
        break;
      case R.id.install_addon:
        installAddon();
        break;
      case R.id.update_addon:
        updateAddon();
        break;
      case R.id.settings:
        openSettingsActivity();
        break;
      case R.id.action_new_tab:
        createNewTab();
        break;
      case R.id.action_close_tab:
        closeTab((TabSession) session);
        break;
      case R.id.save_pdf:
        savePdf(session);
        break;
      case R.id.print_page:
        printPage(session);
        break;
      default:
        return super.onOptionsItemSelected(item);
    }

    return true;
  }

  private void installAddon() {
    final AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle(R.string.install_addon);

    final EditText input = new EditText(this);
    input.setInputType(InputType.TYPE_CLASS_TEXT);
    input.setHint(R.string.install_addon_hint);
    builder.setView(input);

    builder.setPositiveButton(
        R.string.install,
        (dialog, which) -> {
          final String uri = input.getText().toString();

          // We only suopport one extension at a time, so remove the currently installed
          // extension if there is one
          setViewVisibility(mPopupView, false);
          mPopupView = null;
          mPopupSession = null;
          sExtensionManager
              .unregisterExtension()
              .then(
                  unused -> {
                    final WebExtensionController controller =
                        sGeckoRuntime.getWebExtensionController();
                    controller.setPromptDelegate(sExtensionManager);
                    return controller.install(uri);
                  })
              .then(
                  extension ->
                      sGeckoRuntime
                          .getWebExtensionController()
                          .setAllowedInPrivateBrowsing(
                              extension, mAllowExtensionsInPrivateBrowsing.value()))
              .accept(extension -> sExtensionManager.registerExtension(extension));
        });
    builder.setNegativeButton(
        R.string.cancel,
        (dialog, which) -> {
          // Nothing to do
        });

    builder.show();
  }

  private void updateAddon() {
    final AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle(R.string.update_addon);

    sExtensionManager
        .updateExtension()
        .accept(
            extension -> {
              if (extension != null) {
                builder.setMessage("Success");
              } else {
                builder.setMessage("No addon to update");
              }
              builder.show();
            },
            exception -> {
              builder.setMessage("Failed: " + exception);
              builder.show();
            });
  }

  private void createNewTab() {
    Double startTime = sGeckoRuntime.getProfilerController().getProfilerTime();
    TabSession newSession = createSession();
    newSession.open(sGeckoRuntime);
    setGeckoViewSession(newSession);
    mToolbarView.updateTabCount();
    sGeckoRuntime.getProfilerController().addMarker("Create new tab", startTime);
  }

  @SuppressLint("WrongThread")
  @UiThread
  private void savePdf(GeckoSession session) {
    session
        .saveAsPdf()
        .accept(
            pdfStream -> {
              try {
                WebResponse response =
                    new WebResponse.Builder(null)
                        .body(pdfStream)
                        .addHeader("Content-Type", "application/pdf")
                        .addHeader("Content-Disposition", "attachment; filename=PDFDownload.pdf")
                        .build();
                session.getContentDelegate().onExternalResponse(session, response);

              } catch (Exception e) {
                Log.d(LOGTAG, e.getMessage());
              }
            });
  }

  private void printPage(GeckoSession session) {
    session.didPrintPageContent();
  }

  @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();
    }

    @Override
    public void onOrientationUnlock() {
      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
    }
  }

  private class ExampleContentDelegate implements GeckoSession.ContentDelegate {
    @Override
    public void onTitleChange(GeckoSession session, String title) {
      Log.i(LOGTAG, "Content title changed to " + title);
      TabSession tabSession = mTabSessionManager.getSession(session);
      if (tabSession != null) {
        tabSession.setTitle(title);
      }
    }

    @Override
    public void onFullScreen(final GeckoSession session, final boolean fullScreen) {
      getWindow()
          .setFlags(
              fullScreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
              WindowManager.LayoutParams.FLAG_FULLSCREEN);
      mFullScreen = fullScreen;
      if (fullScreen) {
        getSupportActionBar().hide();
      } else {
        getSupportActionBar().show();
      }
    }

    @Override
    public void onFocusRequest(final GeckoSession session) {
      Log.i(LOGTAG, "Content requesting focus");
    }

    @Override
    public void onCloseRequest(final GeckoSession session) {
      final TabSession currentSession = mTabSessionManager.getCurrentSession();
      if (session == currentSession) {
        closeTab(currentSession);
      }
    }

    @Override
    public void onContextMenu(
        final GeckoSession session, int screenX, int screenY, final ContextElement element) {
      Log.d(
          LOGTAG,
          "onContextMenu screenX="
              + screenX
              + " screenY="
              + screenY
              + " type="
              + element.type
              + " linkUri="
              + element.linkUri
              + " title="
              + element.title
              + " alt="
              + element.altText
              + " srcUri="
              + element.srcUri);
    }

    @Override
    public void onExternalResponse(@NonNull GeckoSession session, @NonNull WebResponse response) {
      downloadFile(response);
    }

    @Override
    public void onCrash(GeckoSession session) {
      Log.e(LOGTAG, "Crashed, reopening session");
      session.open(sGeckoRuntime);
    }

    @Override
    public void onKill(GeckoSession session) {
      TabSession tabSession = mTabSessionManager.getSession(session);
      if (tabSession == null) {
        return;
      }

      if (tabSession != mTabSessionManager.getCurrentSession()) {
        Log.e(LOGTAG, "Background session killed");
        return;
      }

      if (isForeground()) {
        throw new IllegalStateException("Foreground content process unexpectedly killed by OS!");
      }

      Log.e(LOGTAG, "Current session killed, reopening");

      tabSession.open(sGeckoRuntime);
      tabSession.loadUri(tabSession.getUri());
    }

    @Override
    public void onFirstComposite(final GeckoSession session) {
      Log.d(LOGTAG, "onFirstComposite");
    }

    @Override
    public void onWebAppManifest(final GeckoSession session, JSONObject manifest) {
      Log.d(LOGTAG, "onWebAppManifest: " + manifest);
    }

    private boolean activeAlert = false;

    @Override
    public GeckoResult<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);
        mGeckoView.setVerticalClipping(0);
      }
    }

    @Override
    public void onCookieBannerDetected(final GeckoSession session) {
      Log.d("BELL", "A cookie banner was detected on this website");
    }

    @Override
    public void onCookieBannerHandled(final GeckoSession session) {
      Log.d("BELL", "A cookie banner was handled on this website");
    }
  }

  private class ExampleProgressDelegate implements GeckoSession.ProgressDelegate {
    private ExampleContentBlockingDelegate mCb;

    private ExampleProgressDelegate(final ExampleContentBlockingDelegate cb) {
      mCb = cb;
    }

    @Override
    public void onPageStart(GeckoSession session, String url) {
      Log.i(LOGTAG, "Starting to load page at " + url);
      Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() + " - page load start");
      mCb.clearCounters();
    }

    @Override
    public void onPageStop(GeckoSession session, boolean success) {
      Log.i(LOGTAG, "Stopping page load " + (success ? "successfully" : "unsuccessfully"));
      Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() + " - page load stop");
      mCb.logCounters();
    }

    @Override
    public void onProgressChange(GeckoSession session, int progress) {
      Log.i(LOGTAG, "onProgressChange " + progress);

      mProgressView.setProgress(progress);

      if (progress > 0 && progress < 100) {
        mProgressView.setVisibility(View.VISIBLE);
      } else {
        mProgressView.setVisibility(View.GONE);
      }
    }

    @Override
    public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) {
      Log.i(LOGTAG, "Security status changed to " + securityInfo.securityMode);
    }

    @Override
    public void onSessionStateChange(GeckoSession session, GeckoSession.SessionState state) {
      Log.i(LOGTAG, "New Session state: " + state.toString());
    }
  }

  private class ExamplePermissionDelegate implements PermissionDelegate {

    public int androidPermissionRequestCode = 1;
    private Callback mCallback;

    class ExampleNotificationCallback implements PermissionDelegate.Callback {
      private final PermissionDelegate.Callback mCallback;

      ExampleNotificationCallback(final PermissionDelegate.Callback callback) {
        mCallback = callback;
      }

      @Override
      public void reject() {
        mShowNotificationsRejected = true;
        mCallback.reject();
      }

      @Override
      public void grant() {
        mShowNotificationsRejected = false;
        mCallback.grant();
      }
    }

    class ExamplePersistentStorageCallback implements PermissionDelegate.Callback {
      private final PermissionDelegate.Callback mCallback;
      private final String mUri;

      ExamplePersistentStorageCallback(final PermissionDelegate.Callback callback, String uri) {
        mCallback = callback;
        mUri = uri;
      }

      @Override
      public void reject() {
        mCallback.reject();
      }

      @Override
      public void grant() {
        mAcceptedPersistentStorage.add(mUri);
        mCallback.grant();
      }
    }

    public void onRequestPermissionsResult(final String[] permissions, final int[] grantResults) {
      if (mCallback == null) {
        return;
      }

      final Callback cb = mCallback;
      mCallback = null;
      for (final int result : grantResults) {
        if (result != PackageManager.PERMISSION_GRANTED) {
          // At least one permission was not granted.
          cb.reject();
          return;
        }
      }
      cb.grant();
    }

    @Override
    public void onAndroidPermissionsRequest(
        final GeckoSession session, final String[] permissions, final Callback callback) {
      if (Build.VERSION.SDK_INT >= 23) {
        // requestPermissions was introduced in API 23.
        mCallback = callback;
        requestPermissions(permissions, androidPermissionRequestCode);
      } else {
        callback.grant();
      }
    }

    @Override
    public GeckoResult<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 class ExampleMediaSessionDelegate implements MediaSession.Delegate {
    private final Activity mActivity;

    public ExampleMediaSessionDelegate(Activity activity) {
      mActivity = activity;
    }

    @Override
    public void onFullscreen(
        @NonNull final GeckoSession session,
        @NonNull final MediaSession mediaSession,
        final boolean enabled,
        @Nullable final MediaSession.ElementMetadata meta) {
      Log.d(LOGTAG, "onFullscreen: Metadata=" + (meta != null ? meta.toString() : "null"));

      if (!enabled) {
        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
        return;
      }

      if (meta == null) {
        return;
      }

      if (meta.width > meta.height) {
        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
      } else {
        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
      }
    }
  }

  private final class ExampleTelemetryDelegate implements RuntimeTelemetry.Delegate {
    @Override
    public void onHistogram(final @NonNull RuntimeTelemetry.Histogram histogram) {
      Log.d(LOGTAG, "onHistogram " + histogram);
    }

    @Override
    public void onBooleanScalar(final @NonNull RuntimeTelemetry.Metric<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);
    }
  }

  private class ExampleActivityDelegate implements GeckoView.ActivityContextDelegate {
    public Context getActivityContext() {
      return GeckoViewActivity.this;
    }
  }
}
