Bug 1414084 - Part 9 - Move add-on menu item cache out of BrowserApp. r=Grisha

Bug 832990 solved the issue of us losing the menu item cache if BrowserApp was
destroyed, however the issue remains that we'll miss any Menu:... messages that
are sent while BrowserApp doesn't exist, e.g. if Gecko is initially loaded
through a GeckoView-based activity.

Therefore we now move the menu item cache and the listener for those messages
into a separate class, whose lifetime better matches that of Gecko.

Apart from any necessary changes, we move the existing code as is. The only
additional change is that we make addAddonMenuItemToMenu() static, because we
can.

MozReview-Commit-ID: BJleonLnjmo

--HG--
extra : rebase_source : e36d954488cc44d250948edcbb8a1964e24ddab7
This commit is contained in:
Jan Henning 2018-02-25 22:22:37 +01:00
parent 98cf3ace3e
commit 24822c48c7
4 changed files with 303 additions and 281 deletions

View File

@ -0,0 +1,296 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import java.util.ArrayList;
import java.util.List;
/**
* For certain UI items added by add-ons or other JS/Gecko code, Gecko notifies us whenever an item
* is added, changed or removed. Since we must not miss any of these notifications and need to re-
* member the current list of active UI items even if e.g. we're in background and our activities
* have been destroyed, we need a class whose lifetime matches (or even exceeds) that of Gecko.
*
* This class fulfills this purpose - it will be initialised early during app startup and just like
* Gecko, once initialised it will remain alive until the OS kills our app.
*
* After initialisation, we will start listening for the appropriate EventDispatcher messages from
* Gecko and maintain an internal list of UI items dynamically added by Gecko.
* In addition, for each class of UI elements an appropriate API will be provided through which the
* intended final consumer can make use of that list in order to actually show those elements in the
* UI.
*/
public class AddonUICache implements BundleEventListener {
private static final String LOGTAG = "GeckoAddonUICache";
private static final int GECKO_TOOLS_MENU_ID = -1;
// When changing this, make sure to adjust NativeWindow.toolsMenuID in browser.js, too.
private static final String GECKO_TOOLS_MENU_UUID = "{115b9308-2023-44f1-a4e9-3e2197669f07}";
private static final int ADDON_MENU_OFFSET = 1000;
private static class MenuItemInfo {
public int id;
public String uuid;
public String label;
public boolean checkable;
public boolean checked;
public boolean enabled = true;
public boolean visible = true;
public int parent;
}
private static final AddonUICache instance = new AddonUICache();
private List<MenuItemInfo> mAddonMenuItemsCache;
private int mAddonMenuNextID = ADDON_MENU_OFFSET;
private Menu mMenu;
private boolean mInitialized;
public static AddonUICache getInstance() {
return instance;
}
private AddonUICache() { }
public void init() {
if (mInitialized) {
return;
}
EventDispatcher.getInstance().registerUiThreadListener(this,
"Menu:Add",
"Menu:Update",
"Menu:Remove",
null);
mInitialized = true;
}
@Override
public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
switch (event) {
case "Menu:Add":
final MenuItemInfo info = new MenuItemInfo();
info.label = message.getString("name");
if (TextUtils.isEmpty(info.label)) {
Log.e(LOGTAG, "Invalid menu item name");
return;
}
info.id = mAddonMenuNextID++;
info.uuid = message.getString("uuid");
info.checked = message.getBoolean("checked", false);
info.enabled = message.getBoolean("enabled", true);
info.visible = message.getBoolean("visible", true);
info.checkable = message.getBoolean("checkable", false);
final String parentUUID = message.getString("parent");
if (GECKO_TOOLS_MENU_UUID.equals(parentUUID)) {
info.parent = GECKO_TOOLS_MENU_ID;
} else if (!TextUtils.isEmpty(parentUUID)) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.uuid.equals(parentUUID)) {
info.parent = item.id;
break;
}
}
}
addAddonMenuItem(info);
break;
case "Menu:Remove":
removeAddonMenuItem(message.getString("uuid"));
break;
case "Menu:Update":
updateAddonMenuItem(message.getString("uuid"),
message.getBundle("options"));
break;
}
}
/**
* Starts handling add-on menu items for the given {@link Menu} and also adds any
* menu items that have already been cached.
*/
public void onCreateOptionsMenu(Menu menu) {
mMenu = menu;
// Add add-on menu items, if any exist.
if (mMenu != null && mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
addAddonMenuItemToMenu(mMenu, item);
}
}
}
/**
* Clears the reference to the Menu passed in {@link AddonUICache#onCreateOptionsMenu}.
* <p>
* Note: Any {@link MenuItem MenuItem(s)} previously added by this class are <i>not</i> removed.
*/
public void onDestroyOptionsMenu() {
mMenu = null;
}
/**
* Adds an addon menu item/webextension browser action to the menu.
*/
private void addAddonMenuItem(final MenuItemInfo info) {
if (mAddonMenuItemsCache == null) {
mAddonMenuItemsCache = new ArrayList<>();
}
mAddonMenuItemsCache.add(info);
if (mMenu == null) {
return;
}
addAddonMenuItemToMenu(mMenu, info);
}
/**
* Removes an addon menu item/webextension browser action from the menu by its UUID.
*/
private void removeAddonMenuItem(String uuid) {
int id = -1;
// Remove add-on menu item from cache, if available.
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.uuid.equals(uuid)) {
id = item.id;
mAddonMenuItemsCache.remove(item);
break;
}
}
}
if (mMenu == null || id == -1) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
mMenu.removeItem(id);
}
}
/**
* Updates the addon menu/webextension browser action with the specified UUID.
*/
private void updateAddonMenuItem(String uuid, final GeckoBundle options) {
int id = -1;
// Set attribute for the menu item in cache, if available
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.uuid.equals(uuid)) {
id = item.id;
item.label = options.getString("name", item.label);
item.checkable = options.getBoolean("checkable", item.checkable);
item.checked = options.getBoolean("checked", item.checked);
item.enabled = options.getBoolean("enabled", item.enabled);
item.visible = options.getBoolean("visible", item.visible);
break;
}
}
}
if (mMenu == null || id == -1) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
}
}
/**
* Add the provided item to the provided menu, which should be
* the root (mMenu).
*/
private static void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
final Menu destination;
if (info.parent == 0) {
destination = menu;
} else if (info.parent == GECKO_TOOLS_MENU_ID) {
// The tools menu only exists in our -v11 resources.
final MenuItem tools = menu.findItem(R.id.tools);
destination = tools != null ? tools.getSubMenu() : menu;
} else {
final MenuItem parent = menu.findItem(info.parent);
if (parent == null) {
return;
}
Menu parentMenu = findParentMenu(menu, parent);
if (!parent.hasSubMenu()) {
parentMenu.removeItem(parent.getItemId());
destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
if (parent.getIcon() != null) {
((SubMenu) destination).getItem().setIcon(parent.getIcon());
}
} else {
destination = parent.getSubMenu();
}
}
final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("item", info.uuid);
EventDispatcher.getInstance().dispatch("Menu:Clicked", data);
return true;
}
});
item.setCheckable(info.checkable);
item.setChecked(info.checked);
item.setEnabled(info.enabled);
item.setVisible(info.visible);
}
private static Menu findParentMenu(Menu menu, MenuItem item) {
final int itemId = item.getItemId();
final int count = (menu != null) ? menu.size() : 0;
for (int i = 0; i < count; i++) {
MenuItem menuItem = menu.getItem(i);
if (menuItem.getItemId() == itemId) {
return menu;
}
if (menuItem.hasSubMenu()) {
Menu parent = findParentMenu(menuItem.getSubMenu(), item);
if (parent != null) {
return parent;
}
}
}
return null;
}
}

View File

@ -34,8 +34,6 @@ import android.nfc.NfcEvent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -56,7 +54,6 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
@ -178,7 +175,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
@ -215,8 +211,6 @@ public class BrowserApp extends GeckoApp
private static final String SWITCHBOARD_SERVER = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records";
private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
private static final String STATE_ADDON_MENU_ITEM_CACHE = "menuitems_cache";
private static final String STATE_ADDON_MENU_NEXT_ID = "menuitems_nextId";
private static final String BROWSER_SEARCH_TAG = "browser_search";
@ -260,10 +254,6 @@ public class BrowserApp extends GeckoApp
private ActionModeCompat mActionMode;
private TabHistoryController tabHistoryController;
private static final int GECKO_TOOLS_MENU_ID = -1;
// When changing this, make sure to adjust NativeWindow.toolsMenuID in browser.js, too.
private static final String GECKO_TOOLS_MENU_UUID = "{115b9308-2023-44f1-a4e9-3e2197669f07}";
private static final int ADDON_MENU_OFFSET = 1000;
public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";
// When the static action bar is shown, only the real toolbar chrome should be
@ -276,68 +266,12 @@ public class BrowserApp extends GeckoApp
private Intent startingIntentAfterPip;
private boolean isInAutomation;
private static class MenuItemInfo implements Parcelable {
public int id;
public String uuid;
public String label;
public boolean checkable;
public boolean checked;
public boolean enabled = true;
public boolean visible = true;
public int parent;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(uuid);
dest.writeString(label);
dest.writeInt(checkable ? 1 : 0);
dest.writeInt(checked ? 1 : 0);
dest.writeInt(enabled ? 1 : 0);
dest.writeInt(visible ? 1 : 0);
dest.writeInt(parent);
}
public static final Parcelable.Creator<MenuItemInfo> CREATOR
= new Parcelable.Creator<MenuItemInfo>() {
@Override
public MenuItemInfo createFromParcel(Parcel source) {
return new MenuItemInfo(source);
}
@Override
public MenuItemInfo[] newArray(int size) {
return new MenuItemInfo[size];
}
};
private MenuItemInfo(Parcel source) {
id = source.readInt();
uuid = source.readString();
label = source.readString();
checkable = source.readInt() != 0;
checked = source.readInt() != 0;
enabled = source.readInt() != 0;
visible = source.readInt() != 0;
parent = source.readInt();
}
public MenuItemInfo() { }
}
// The types of guest mode dialogs we show.
public static enum GuestModeDialog {
ENTERING,
LEAVING
}
private ArrayList<MenuItemInfo> mAddonMenuItemsCache;
private int mAddonMenuNextID = ADDON_MENU_OFFSET;
private PropertyAnimator mMainLayoutAnimator;
private static final Interpolator sTabsInterpolator = new Interpolator() {
@ -744,14 +678,6 @@ public class BrowserApp extends GeckoApp
}
});
// If the activity is being restored, the add-ons menu item cache only needs restoring if
// Gecko is already running. Otherwise, we'll simply catch the corresponding events when
// Gecko and the add-ons are starting up.
if (savedInstanceState != null && mIsRestoringActivity) {
mAddonMenuItemsCache = savedInstanceState.getParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE);
mAddonMenuNextID = savedInstanceState.getInt(STATE_ADDON_MENU_NEXT_ID);
}
app.getLightweightTheme().addListener(this);
mProgressView = (AnimatedProgressBar) findViewById(R.id.page_progress);
@ -853,9 +779,6 @@ public class BrowserApp extends GeckoApp
EventDispatcher.getInstance().registerUiThreadListener(this,
"GeckoView:AccessibilityEnabled",
"Menu:Open",
"Menu:Update",
"Menu:Add",
"Menu:Remove",
"LightweightTheme:Update",
"Tab:Added",
"Video:Play",
@ -1547,6 +1470,8 @@ public class BrowserApp extends GeckoApp
final GeckoApplication app = (GeckoApplication) getApplication();
app.getLightweightTheme().removeListener(this);
AddonUICache.getInstance().onDestroyOptionsMenu();
if (mBrowserToolbar != null)
mBrowserToolbar.onDestroy();
@ -1589,9 +1514,6 @@ public class BrowserApp extends GeckoApp
EventDispatcher.getInstance().unregisterUiThreadListener(this,
"GeckoView:AccessibilityEnabled",
"Menu:Open",
"Menu:Update",
"Menu:Add",
"Menu:Remove",
"LightweightTheme:Update",
"Tab:Added",
"Video:Play",
@ -1850,42 +1772,6 @@ public class BrowserApp extends GeckoApp
openOptionsMenu();
break;
case "Menu:Add":
final MenuItemInfo info = new MenuItemInfo();
info.label = message.getString("name");
if (TextUtils.isEmpty(info.label)) {
Log.e(LOGTAG, "Invalid menu item name");
return;
}
info.id = mAddonMenuNextID++;
info.uuid = message.getString("uuid");
info.checked = message.getBoolean("checked", false);
info.enabled = message.getBoolean("enabled", true);
info.visible = message.getBoolean("visible", true);
info.checkable = message.getBoolean("checkable", false);
final String parentUUID = message.getString("parent");
if (GECKO_TOOLS_MENU_UUID.equals(parentUUID)) {
info.parent = GECKO_TOOLS_MENU_ID;
} else if (!TextUtils.isEmpty(parentUUID)) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.uuid.equals(parentUUID)) {
info.parent = item.id;
break;
}
}
}
addAddonMenuItem(info);
break;
case "Menu:Remove":
removeAddonMenuItem(message.getString("uuid"));
break;
case "Menu:Update":
updateAddonMenuItem(message.getString("uuid"),
message.getBundle("options"));
break;
case "LightweightTheme:Update":
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
break;
@ -2407,15 +2293,6 @@ public class BrowserApp extends GeckoApp
super.onSaveInstanceState(outState);
mDynamicToolbar.onSaveInstanceState(outState);
outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomeScreenContainer.getPaddingTop());
// The various add-on UI item caches and event listeners should really live somewhere based
// on the Application, so that their lifetime more closely matches that of Gecko itself, as
// GeckoView-based activities can start Gecko (and therefore add-ons) while BrowserApp isn't
// even running.
// For now we'll only guard against the case where BrowserApp is destroyed and later re-
// created while Gecko keeps running throughout, and leave the full solution to bug 1414084.
outState.putParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE, mAddonMenuItemsCache);
outState.putInt(STATE_ADDON_MENU_NEXT_ID, mAddonMenuNextID);
}
/**
@ -3166,155 +3043,6 @@ public class BrowserApp extends GeckoApp
}
}
private static Menu findParentMenu(Menu menu, MenuItem item) {
final int itemId = item.getItemId();
final int count = (menu != null) ? menu.size() : 0;
for (int i = 0; i < count; i++) {
MenuItem menuItem = menu.getItem(i);
if (menuItem.getItemId() == itemId) {
return menu;
}
if (menuItem.hasSubMenu()) {
Menu parent = findParentMenu(menuItem.getSubMenu(), item);
if (parent != null) {
return parent;
}
}
}
return null;
}
/**
* Add the provided item to the provided menu, which should be
* the root (mMenu).
*/
private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
final Menu destination;
if (info.parent == 0) {
destination = menu;
} else if (info.parent == GECKO_TOOLS_MENU_ID) {
// The tools menu only exists in our -v11 resources.
final MenuItem tools = menu.findItem(R.id.tools);
destination = tools != null ? tools.getSubMenu() : menu;
} else {
final MenuItem parent = menu.findItem(info.parent);
if (parent == null) {
return;
}
Menu parentMenu = findParentMenu(menu, parent);
if (!parent.hasSubMenu()) {
parentMenu.removeItem(parent.getItemId());
destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
if (parent.getIcon() != null) {
((SubMenu) destination).getItem().setIcon(parent.getIcon());
}
} else {
destination = parent.getSubMenu();
}
}
final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("item", info.uuid);
EventDispatcher.getInstance().dispatch("Menu:Clicked", data);
return true;
}
});
item.setCheckable(info.checkable);
item.setChecked(info.checked);
item.setEnabled(info.enabled);
item.setVisible(info.visible);
}
/**
* Adds an addon menu item/webextension browser action to the menu.
*/
private void addAddonMenuItem(final MenuItemInfo info) {
if (mAddonMenuItemsCache == null) {
mAddonMenuItemsCache = new ArrayList<>();
}
// Always cache so we can rebuild after a locale switch.
mAddonMenuItemsCache.add(info);
if (mMenu == null) {
return;
}
addAddonMenuItemToMenu(mMenu, info);
}
/**
* Removes an addon menu item/webextension browser action from the menu by its UUID.
*/
private void removeAddonMenuItem(String uuid) {
int id = -1;
// Remove add-on menu item from cache, if available.
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.uuid.equals(uuid)) {
id = item.id;
mAddonMenuItemsCache.remove(item);
break;
}
}
}
if (mMenu == null || id == -1) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
mMenu.removeItem(id);
}
}
/**
* Updates the addon menu/webextension browser action with the specified UUID.
*/
private void updateAddonMenuItem(String uuid, final GeckoBundle options) {
int id = -1;
// Set attribute for the menu item in cache, if available
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.uuid.equals(uuid)) {
id = item.id;
item.label = options.getString("name", item.label);
item.checkable = options.getBoolean("checkable", item.checkable);
item.checked = options.getBoolean("checked", item.checked);
item.enabled = options.getBoolean("enabled", item.enabled);
item.visible = options.getBoolean("visible", item.visible);
break;
}
}
}
if (mMenu == null || id == -1) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Sets mMenu = menu.
@ -3329,12 +3057,9 @@ public class BrowserApp extends GeckoApp
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.browser_app_menu, mMenu);
// Add add-on menu items, if any exist.
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
addAddonMenuItemToMenu(mMenu, item);
}
}
// Let the AddonUICache handle adding (and removing again) any add-on/browser action
// menu items as required.
AddonUICache.getInstance().onCreateOptionsMenu(mMenu);
// Action providers are available only ICS+.
GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);

View File

@ -334,6 +334,7 @@ public class GeckoApplication extends Application
FilePicker.init(context);
DownloadsIntegration.init();
HomePanelsManager.getInstance().init(context);
AddonUICache.getInstance().init();
GlobalPageMetadata.getInstance().init();

View File

@ -2336,7 +2336,7 @@ var NativeWindow = {
menu: {
_callbacks: [],
// This value must be kept in sync with GECKO_TOOLS_MENU_UUID in BrowserApp.java.
// This value must be kept in sync with GECKO_TOOLS_MENU_UUID in AddonUICache.java.
toolsMenuID: "{115b9308-2023-44f1-a4e9-3e2197669f07}",
add: function() {
let options;