/* -*- 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.gecko.home; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.gecko.annotation.RobocopTarget; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.R; import org.mozilla.gecko.util.ThreadUtils; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Pair; public final class HomeConfig { public static final String PREF_KEY_BOOKMARKS_PANEL_ENABLED = "bookmarksPanelEnabled"; public static final String PREF_KEY_HISTORY_PANEL_ENABLED = "combinedHistoryPanelEnabled"; /** * Used to determine what type of HomeFragment subclass to use when creating * a given panel. With the exception of DYNAMIC, all of these types correspond * to a default set of built-in panels. The DYNAMIC panel type is used by * third-party services to create panels with varying types of content. */ @RobocopTarget public static enum PanelType implements Parcelable { TOP_SITES("top_sites", TopSitesPanel.class), BOOKMARKS("bookmarks", BookmarksPanel.class), COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class), DYNAMIC("dynamic", DynamicPanel.class), // Deprecated panels that should no longer exist but are kept around for // migration code. Class references have been replaced with new version of the panel. DEPRECATED_REMOTE_TABS("remote_tabs", CombinedHistoryPanel.class), DEPRECATED_HISTORY("history", CombinedHistoryPanel.class), DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class), DEPRECATED_RECENT_TABS("recent_tabs", CombinedHistoryPanel.class); private final String mId; private final Class mPanelClass; PanelType(String id, Class panelClass) { mId = id; mPanelClass = panelClass; } public static PanelType fromId(String id) { if (id == null) { throw new IllegalArgumentException("Could not convert null String to PanelType"); } for (PanelType panelType : PanelType.values()) { if (TextUtils.equals(panelType.mId, id.toLowerCase())) { return panelType; } } throw new IllegalArgumentException("Could not convert String id to PanelType"); } @Override public String toString() { return mId; } public Class getPanelClass() { return mPanelClass; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } public static final Creator CREATOR = new Creator() { @Override public PanelType createFromParcel(final Parcel source) { return PanelType.values()[source.readInt()]; } @Override public PanelType[] newArray(final int size) { return new PanelType[size]; } }; } public static class PanelConfig implements Parcelable { private final PanelType mType; private final String mTitle; private final String mId; private final LayoutType mLayoutType; private final List mViews; private final AuthConfig mAuthConfig; private final EnumSet mFlags; private final int mPosition; static final String JSON_KEY_TYPE = "type"; static final String JSON_KEY_TITLE = "title"; static final String JSON_KEY_ID = "id"; static final String JSON_KEY_LAYOUT = "layout"; static final String JSON_KEY_VIEWS = "views"; static final String JSON_KEY_AUTH_CONFIG = "authConfig"; static final String JSON_KEY_DEFAULT = "default"; static final String JSON_KEY_DISABLED = "disabled"; static final String JSON_KEY_POSITION = "position"; public enum Flags { DEFAULT_PANEL, DISABLED_PANEL } public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException { final String panelType = json.optString(JSON_KEY_TYPE, null); if (TextUtils.isEmpty(panelType)) { mType = PanelType.DYNAMIC; } else { mType = PanelType.fromId(panelType); } mTitle = json.getString(JSON_KEY_TITLE); mId = json.getString(JSON_KEY_ID); final String layoutTypeId = json.optString(JSON_KEY_LAYOUT, null); if (layoutTypeId != null) { mLayoutType = LayoutType.fromId(layoutTypeId); } else { mLayoutType = null; } final JSONArray jsonViews = json.optJSONArray(JSON_KEY_VIEWS); if (jsonViews != null) { mViews = new ArrayList(); final int viewCount = jsonViews.length(); for (int i = 0; i < viewCount; i++) { final JSONObject jsonViewConfig = (JSONObject) jsonViews.get(i); final ViewConfig viewConfig = new ViewConfig(i, jsonViewConfig); mViews.add(viewConfig); } } else { mViews = null; } final JSONObject jsonAuthConfig = json.optJSONObject(JSON_KEY_AUTH_CONFIG); if (jsonAuthConfig != null) { mAuthConfig = new AuthConfig(jsonAuthConfig); } else { mAuthConfig = null; } mFlags = EnumSet.noneOf(Flags.class); if (json.optBoolean(JSON_KEY_DEFAULT, false)) { mFlags.add(Flags.DEFAULT_PANEL); } if (json.optBoolean(JSON_KEY_DISABLED, false)) { mFlags.add(Flags.DISABLED_PANEL); } mPosition = json.optInt(JSON_KEY_POSITION, -1); validate(); } @SuppressWarnings("unchecked") public PanelConfig(Parcel in) { mType = (PanelType) in.readParcelable(getClass().getClassLoader()); mTitle = in.readString(); mId = in.readString(); mLayoutType = (LayoutType) in.readParcelable(getClass().getClassLoader()); mViews = new ArrayList(); in.readTypedList(mViews, ViewConfig.CREATOR); mAuthConfig = (AuthConfig) in.readParcelable(getClass().getClassLoader()); mFlags = (EnumSet) in.readSerializable(); mPosition = in.readInt(); validate(); } public PanelConfig(PanelConfig panelConfig) { mType = panelConfig.mType; mTitle = panelConfig.mTitle; mId = panelConfig.mId; mLayoutType = panelConfig.mLayoutType; mViews = new ArrayList(); List viewConfigs = panelConfig.mViews; if (viewConfigs != null) { for (ViewConfig viewConfig : viewConfigs) { mViews.add(new ViewConfig(viewConfig)); } } mAuthConfig = panelConfig.mAuthConfig; mFlags = panelConfig.mFlags.clone(); mPosition = panelConfig.mPosition; validate(); } public PanelConfig(PanelType type, String title, String id) { this(type, title, id, EnumSet.noneOf(Flags.class)); } public PanelConfig(PanelType type, String title, String id, EnumSet flags) { this(type, title, id, null, null, null, flags, -1); } public PanelConfig(PanelType type, String title, String id, LayoutType layoutType, List views, AuthConfig authConfig, EnumSet flags, int position) { mType = type; mTitle = title; mId = id; mLayoutType = layoutType; mViews = views; mAuthConfig = authConfig; mFlags = flags; mPosition = position; validate(); } private void validate() { if (mType == null) { throw new IllegalArgumentException("Can't create PanelConfig with null type"); } if (TextUtils.isEmpty(mTitle)) { throw new IllegalArgumentException("Can't create PanelConfig with empty title"); } if (TextUtils.isEmpty(mId)) { throw new IllegalArgumentException("Can't create PanelConfig with empty id"); } if (mLayoutType == null && mType == PanelType.DYNAMIC) { throw new IllegalArgumentException("Can't create a dynamic PanelConfig with null layout type"); } if ((mViews == null || mViews.size() == 0) && mType == PanelType.DYNAMIC) { throw new IllegalArgumentException("Can't create a dynamic PanelConfig with no views"); } if (mFlags == null) { throw new IllegalArgumentException("Can't create PanelConfig with null flags"); } } public PanelType getType() { return mType; } public String getTitle() { return mTitle; } public String getId() { return mId; } public LayoutType getLayoutType() { return mLayoutType; } public int getViewCount() { return (mViews != null ? mViews.size() : 0); } public ViewConfig getViewAt(int index) { return (mViews != null ? mViews.get(index) : null); } public EnumSet getFlags() { return mFlags.clone(); } public boolean isDynamic() { return (mType == PanelType.DYNAMIC); } public boolean isDefault() { return mFlags.contains(Flags.DEFAULT_PANEL); } private void setIsDefault(boolean isDefault) { if (isDefault) { mFlags.add(Flags.DEFAULT_PANEL); } else { mFlags.remove(Flags.DEFAULT_PANEL); } } public boolean isDisabled() { return mFlags.contains(Flags.DISABLED_PANEL); } private void setIsDisabled(boolean isDisabled) { if (isDisabled) { mFlags.add(Flags.DISABLED_PANEL); } else { mFlags.remove(Flags.DISABLED_PANEL); } } public AuthConfig getAuthConfig() { return mAuthConfig; } public int getPosition() { return mPosition; } public JSONObject toJSON() throws JSONException { final JSONObject json = new JSONObject(); json.put(JSON_KEY_TYPE, mType.toString()); json.put(JSON_KEY_TITLE, mTitle); json.put(JSON_KEY_ID, mId); if (mLayoutType != null) { json.put(JSON_KEY_LAYOUT, mLayoutType.toString()); } if (mViews != null) { final JSONArray jsonViews = new JSONArray(); final int viewCount = mViews.size(); for (int i = 0; i < viewCount; i++) { final ViewConfig viewConfig = mViews.get(i); final JSONObject jsonViewConfig = viewConfig.toJSON(); jsonViews.put(jsonViewConfig); } json.put(JSON_KEY_VIEWS, jsonViews); } if (mAuthConfig != null) { json.put(JSON_KEY_AUTH_CONFIG, mAuthConfig.toJSON()); } if (mFlags.contains(Flags.DEFAULT_PANEL)) { json.put(JSON_KEY_DEFAULT, true); } if (mFlags.contains(Flags.DISABLED_PANEL)) { json.put(JSON_KEY_DISABLED, true); } json.put(JSON_KEY_POSITION, mPosition); return json; } @Override public boolean equals(Object o) { if (o == null) { return false; } if (this == o) { return true; } if (!(o instanceof PanelConfig)) { return false; } final PanelConfig other = (PanelConfig) o; return mId.equals(other.mId); } @Override public int hashCode() { return super.hashCode(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(mType, 0); dest.writeString(mTitle); dest.writeString(mId); dest.writeParcelable(mLayoutType, 0); dest.writeTypedList(mViews); dest.writeParcelable(mAuthConfig, 0); dest.writeSerializable(mFlags); dest.writeInt(mPosition); } public static final Creator CREATOR = new Creator() { @Override public PanelConfig createFromParcel(final Parcel in) { return new PanelConfig(in); } @Override public PanelConfig[] newArray(final int size) { return new PanelConfig[size]; } }; } public static enum LayoutType implements Parcelable { FRAME("frame"); private final String mId; LayoutType(String id) { mId = id; } public static LayoutType fromId(String id) { if (id == null) { throw new IllegalArgumentException("Could not convert null String to LayoutType"); } for (LayoutType layoutType : LayoutType.values()) { if (TextUtils.equals(layoutType.mId, id.toLowerCase())) { return layoutType; } } throw new IllegalArgumentException("Could not convert String id to LayoutType"); } @Override public String toString() { return mId; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } public static final Creator CREATOR = new Creator() { @Override public LayoutType createFromParcel(final Parcel source) { return LayoutType.values()[source.readInt()]; } @Override public LayoutType[] newArray(final int size) { return new LayoutType[size]; } }; } public static enum ViewType implements Parcelable { LIST("list"), GRID("grid"); private final String mId; ViewType(String id) { mId = id; } public static ViewType fromId(String id) { if (id == null) { throw new IllegalArgumentException("Could not convert null String to ViewType"); } for (ViewType viewType : ViewType.values()) { if (TextUtils.equals(viewType.mId, id.toLowerCase())) { return viewType; } } throw new IllegalArgumentException("Could not convert String id to ViewType"); } @Override public String toString() { return mId; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } public static final Creator CREATOR = new Creator() { @Override public ViewType createFromParcel(final Parcel source) { return ViewType.values()[source.readInt()]; } @Override public ViewType[] newArray(final int size) { return new ViewType[size]; } }; } public static enum ItemType implements Parcelable { ARTICLE("article"), IMAGE("image"), ICON("icon"); private final String mId; ItemType(String id) { mId = id; } public static ItemType fromId(String id) { if (id == null) { throw new IllegalArgumentException("Could not convert null String to ItemType"); } for (ItemType itemType : ItemType.values()) { if (TextUtils.equals(itemType.mId, id.toLowerCase())) { return itemType; } } throw new IllegalArgumentException("Could not convert String id to ItemType"); } @Override public String toString() { return mId; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } public static final Creator CREATOR = new Creator() { @Override public ItemType createFromParcel(final Parcel source) { return ItemType.values()[source.readInt()]; } @Override public ItemType[] newArray(final int size) { return new ItemType[size]; } }; } public static enum ItemHandler implements Parcelable { BROWSER("browser"), INTENT("intent"); private final String mId; ItemHandler(String id) { mId = id; } public static ItemHandler fromId(String id) { if (id == null) { throw new IllegalArgumentException("Could not convert null String to ItemHandler"); } for (ItemHandler itemHandler : ItemHandler.values()) { if (TextUtils.equals(itemHandler.mId, id.toLowerCase())) { return itemHandler; } } throw new IllegalArgumentException("Could not convert String id to ItemHandler"); } @Override public String toString() { return mId; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } public static final Creator CREATOR = new Creator() { @Override public ItemHandler createFromParcel(final Parcel source) { return ItemHandler.values()[source.readInt()]; } @Override public ItemHandler[] newArray(final int size) { return new ItemHandler[size]; } }; } public static class ViewConfig implements Parcelable { private final int mIndex; private final ViewType mType; private final String mDatasetId; private final ItemType mItemType; private final ItemHandler mItemHandler; private final String mBackImageUrl; private final String mFilter; private final EmptyViewConfig mEmptyViewConfig; private final HeaderConfig mHeaderConfig; private final EnumSet mFlags; static final String JSON_KEY_TYPE = "type"; static final String JSON_KEY_DATASET = "dataset"; static final String JSON_KEY_ITEM_TYPE = "itemType"; static final String JSON_KEY_ITEM_HANDLER = "itemHandler"; static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl"; static final String JSON_KEY_FILTER = "filter"; static final String JSON_KEY_EMPTY = "empty"; static final String JSON_KEY_HEADER = "header"; static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled"; public enum Flags { REFRESH_ENABLED } public ViewConfig(int index, JSONObject json) throws JSONException, IllegalArgumentException { mIndex = index; mType = ViewType.fromId(json.getString(JSON_KEY_TYPE)); mDatasetId = json.getString(JSON_KEY_DATASET); mItemType = ItemType.fromId(json.getString(JSON_KEY_ITEM_TYPE)); mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER)); mBackImageUrl = json.optString(JSON_KEY_BACK_IMAGE_URL, null); mFilter = json.optString(JSON_KEY_FILTER, null); final JSONObject jsonEmptyViewConfig = json.optJSONObject(JSON_KEY_EMPTY); if (jsonEmptyViewConfig != null) { mEmptyViewConfig = new EmptyViewConfig(jsonEmptyViewConfig); } else { mEmptyViewConfig = null; } final JSONObject jsonHeaderConfig = json.optJSONObject(JSON_KEY_HEADER); mHeaderConfig = jsonHeaderConfig != null ? new HeaderConfig(jsonHeaderConfig) : null; mFlags = EnumSet.noneOf(Flags.class); if (json.optBoolean(JSON_KEY_REFRESH_ENABLED, false)) { mFlags.add(Flags.REFRESH_ENABLED); } validate(); } @SuppressWarnings("unchecked") public ViewConfig(Parcel in) { mIndex = in.readInt(); mType = (ViewType) in.readParcelable(getClass().getClassLoader()); mDatasetId = in.readString(); mItemType = (ItemType) in.readParcelable(getClass().getClassLoader()); mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader()); mBackImageUrl = in.readString(); mFilter = in.readString(); mEmptyViewConfig = (EmptyViewConfig) in.readParcelable(getClass().getClassLoader()); mHeaderConfig = (HeaderConfig) in.readParcelable(getClass().getClassLoader()); mFlags = (EnumSet) in.readSerializable(); validate(); } public ViewConfig(ViewConfig viewConfig) { mIndex = viewConfig.mIndex; mType = viewConfig.mType; mDatasetId = viewConfig.mDatasetId; mItemType = viewConfig.mItemType; mItemHandler = viewConfig.mItemHandler; mBackImageUrl = viewConfig.mBackImageUrl; mFilter = viewConfig.mFilter; mEmptyViewConfig = viewConfig.mEmptyViewConfig; mHeaderConfig = viewConfig.mHeaderConfig; mFlags = viewConfig.mFlags.clone(); validate(); } private void validate() { if (mType == null) { throw new IllegalArgumentException("Can't create ViewConfig with null type"); } if (TextUtils.isEmpty(mDatasetId)) { throw new IllegalArgumentException("Can't create ViewConfig with empty dataset ID"); } if (mItemType == null) { throw new IllegalArgumentException("Can't create ViewConfig with null item type"); } if (mItemHandler == null) { throw new IllegalArgumentException("Can't create ViewConfig with null item handler"); } if (mFlags == null) { throw new IllegalArgumentException("Can't create ViewConfig with null flags"); } } public int getIndex() { return mIndex; } public ViewType getType() { return mType; } public String getDatasetId() { return mDatasetId; } public ItemType getItemType() { return mItemType; } public ItemHandler getItemHandler() { return mItemHandler; } public String getBackImageUrl() { return mBackImageUrl; } public String getFilter() { return mFilter; } public EmptyViewConfig getEmptyViewConfig() { return mEmptyViewConfig; } public HeaderConfig getHeaderConfig() { return mHeaderConfig; } public boolean hasHeaderConfig() { return mHeaderConfig != null; } public boolean isRefreshEnabled() { return mFlags.contains(Flags.REFRESH_ENABLED); } public JSONObject toJSON() throws JSONException { final JSONObject json = new JSONObject(); json.put(JSON_KEY_TYPE, mType.toString()); json.put(JSON_KEY_DATASET, mDatasetId); json.put(JSON_KEY_ITEM_TYPE, mItemType.toString()); json.put(JSON_KEY_ITEM_HANDLER, mItemHandler.toString()); if (!TextUtils.isEmpty(mBackImageUrl)) { json.put(JSON_KEY_BACK_IMAGE_URL, mBackImageUrl); } if (!TextUtils.isEmpty(mFilter)) { json.put(JSON_KEY_FILTER, mFilter); } if (mEmptyViewConfig != null) { json.put(JSON_KEY_EMPTY, mEmptyViewConfig.toJSON()); } if (mHeaderConfig != null) { json.put(JSON_KEY_HEADER, mHeaderConfig.toJSON()); } if (mFlags.contains(Flags.REFRESH_ENABLED)) { json.put(JSON_KEY_REFRESH_ENABLED, true); } return json; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mIndex); dest.writeParcelable(mType, 0); dest.writeString(mDatasetId); dest.writeParcelable(mItemType, 0); dest.writeParcelable(mItemHandler, 0); dest.writeString(mBackImageUrl); dest.writeString(mFilter); dest.writeParcelable(mEmptyViewConfig, 0); dest.writeParcelable(mHeaderConfig, 0); dest.writeSerializable(mFlags); } public static final Creator CREATOR = new Creator() { @Override public ViewConfig createFromParcel(final Parcel in) { return new ViewConfig(in); } @Override public ViewConfig[] newArray(final int size) { return new ViewConfig[size]; } }; } public static class EmptyViewConfig implements Parcelable { private final String mText; private final String mImageUrl; static final String JSON_KEY_TEXT = "text"; static final String JSON_KEY_IMAGE_URL = "imageUrl"; public EmptyViewConfig(JSONObject json) throws JSONException, IllegalArgumentException { mText = json.optString(JSON_KEY_TEXT, null); mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null); } public EmptyViewConfig(Parcel in) { mText = in.readString(); mImageUrl = in.readString(); } public EmptyViewConfig(EmptyViewConfig emptyViewConfig) { mText = emptyViewConfig.mText; mImageUrl = emptyViewConfig.mImageUrl; } public EmptyViewConfig(String text, String imageUrl) { mText = text; mImageUrl = imageUrl; } public String getText() { return mText; } public String getImageUrl() { return mImageUrl; } public JSONObject toJSON() throws JSONException { final JSONObject json = new JSONObject(); json.put(JSON_KEY_TEXT, mText); json.put(JSON_KEY_IMAGE_URL, mImageUrl); return json; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mText); dest.writeString(mImageUrl); } public static final Creator CREATOR = new Creator() { @Override public EmptyViewConfig createFromParcel(final Parcel in) { return new EmptyViewConfig(in); } @Override public EmptyViewConfig[] newArray(final int size) { return new EmptyViewConfig[size]; } }; } public static class HeaderConfig implements Parcelable { static final String JSON_KEY_IMAGE_URL = "image_url"; static final String JSON_KEY_URL = "url"; private final String mImageUrl; private final String mUrl; public HeaderConfig(JSONObject json) { mImageUrl = json.optString(JSON_KEY_IMAGE_URL); mUrl = json.optString(JSON_KEY_URL); } public HeaderConfig(Parcel in) { mImageUrl = in.readString(); mUrl = in.readString(); } public String getImageUrl() { return mImageUrl; } public String getUrl() { return mUrl; } public JSONObject toJSON() throws JSONException { JSONObject json = new JSONObject(); json.put(JSON_KEY_IMAGE_URL, mImageUrl); json.put(JSON_KEY_URL, mUrl); return json; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mImageUrl); dest.writeString(mUrl); } public static final Creator CREATOR = new Creator() { @Override public HeaderConfig createFromParcel(Parcel source) { return new HeaderConfig(source); } @Override public HeaderConfig[] newArray(int size) { return new HeaderConfig[size]; } }; } public static class AuthConfig implements Parcelable { private final String mMessageText; private final String mButtonText; private final String mImageUrl; static final String JSON_KEY_MESSAGE_TEXT = "messageText"; static final String JSON_KEY_BUTTON_TEXT = "buttonText"; static final String JSON_KEY_IMAGE_URL = "imageUrl"; public AuthConfig(JSONObject json) throws JSONException, IllegalArgumentException { mMessageText = json.optString(JSON_KEY_MESSAGE_TEXT); mButtonText = json.optString(JSON_KEY_BUTTON_TEXT); mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null); } public AuthConfig(Parcel in) { mMessageText = in.readString(); mButtonText = in.readString(); mImageUrl = in.readString(); validate(); } public AuthConfig(AuthConfig authConfig) { mMessageText = authConfig.mMessageText; mButtonText = authConfig.mButtonText; mImageUrl = authConfig.mImageUrl; validate(); } public AuthConfig(String messageText, String buttonText, String imageUrl) { mMessageText = messageText; mButtonText = buttonText; mImageUrl = imageUrl; validate(); } private void validate() { if (mMessageText == null) { throw new IllegalArgumentException("Can't create AuthConfig with null message text"); } if (mButtonText == null) { throw new IllegalArgumentException("Can't create AuthConfig with null button text"); } } public String getMessageText() { return mMessageText; } public String getButtonText() { return mButtonText; } public String getImageUrl() { return mImageUrl; } public JSONObject toJSON() throws JSONException { final JSONObject json = new JSONObject(); json.put(JSON_KEY_MESSAGE_TEXT, mMessageText); json.put(JSON_KEY_BUTTON_TEXT, mButtonText); json.put(JSON_KEY_IMAGE_URL, mImageUrl); return json; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mMessageText); dest.writeString(mButtonText); dest.writeString(mImageUrl); } public static final Creator CREATOR = new Creator() { @Override public AuthConfig createFromParcel(final Parcel in) { return new AuthConfig(in); } @Override public AuthConfig[] newArray(final int size) { return new AuthConfig[size]; } }; } /** * Immutable representation of the current state of {@code HomeConfig}. * This is what HomeConfig returns from a load() call and takes as * input to save a new state. * * Users of {@code State} should use an {@code Iterator} to iterate * through the contained {@code PanelConfig} instances. * * {@code State} is immutable i.e. you can't add, remove, or update * contained elements directly. You have to use an {@code Editor} to * change the state, which can be created through the {@code edit()} * method. */ public static class State implements Iterable { private HomeConfig mHomeConfig; private final List mPanelConfigs; private final boolean mIsDefault; State(List panelConfigs, boolean isDefault) { this(null, panelConfigs, isDefault); } private State(HomeConfig homeConfig, List panelConfigs, boolean isDefault) { mHomeConfig = homeConfig; mPanelConfigs = Collections.unmodifiableList(panelConfigs); mIsDefault = isDefault; } private void setHomeConfig(HomeConfig homeConfig) { if (mHomeConfig != null) { throw new IllegalStateException("Can't set HomeConfig more than once"); } mHomeConfig = homeConfig; } @Override public Iterator iterator() { return mPanelConfigs.iterator(); } /** * Returns whether this {@code State} instance represents the default * {@code HomeConfig} configuration or not. */ public boolean isDefault() { return mIsDefault; } /** * Creates an {@code Editor} for this state. */ public Editor edit() { return new Editor(mHomeConfig, this); } } /** * {@code Editor} allows you to make changes to a {@code State}. You * can create {@code Editor} by calling {@code edit()} on the target * {@code State} instance. * * {@code Editor} works on a copy of the {@code State} that originated * it. This means that adding, removing, or updating panels in an * {@code Editor} will never change the {@code State} which you * created the {@code Editor} from. Calling {@code commit()} or * {@code apply()} will cause the new {@code State} instance to be * created and saved using the {@code HomeConfig} instance that * created the source {@code State}. * * {@code Editor} is *not* thread-safe. You can only make calls on it * from the thread where it was originally created. It will throw an * exception if you don't follow this invariant. */ public static class Editor implements Iterable { private final HomeConfig mHomeConfig; private final Map mConfigMap; private final List mConfigOrder; private final Thread mOriginalThread; // Each Pair represents parameters to a GeckoAppShell.notifyObservers call; // the first String is the observer topic and the second string is the notification data. private List> mNotificationQueue; private PanelConfig mDefaultPanel; private int mEnabledCount; private boolean mHasChanged; private final boolean mIsFromDefault; private Editor(HomeConfig homeConfig, State configState) { mHomeConfig = homeConfig; mOriginalThread = Thread.currentThread(); mConfigMap = new HashMap(); mConfigOrder = new LinkedList(); mNotificationQueue = new ArrayList<>(); mIsFromDefault = configState.isDefault(); initFromState(configState); } /** * Initialize the initial state of the editor from the given * {@sode State}. A HashMap is used to represent the list of * panels as it provides fast access, and a LinkedList is used to * keep track of order. We keep a reference to the default panel * and the number of enabled panels to avoid iterating through the * map every time we need those. * * @param configState The source State to load the editor from. */ private void initFromState(State configState) { for (PanelConfig panelConfig : configState) { final PanelConfig panelCopy = new PanelConfig(panelConfig); if (!panelCopy.isDisabled()) { mEnabledCount++; } if (panelCopy.isDefault()) { if (mDefaultPanel == null) { mDefaultPanel = panelCopy; } else { throw new IllegalStateException("Multiple default panels in HomeConfig state"); } } final String panelId = panelConfig.getId(); mConfigOrder.add(panelId); mConfigMap.put(panelId, panelCopy); } // We should always have a defined default panel if there's // at least one enabled panel around. if (mEnabledCount > 0 && mDefaultPanel == null) { throw new IllegalStateException("Default panel in HomeConfig state is undefined"); } } private PanelConfig getPanelOrThrow(String panelId) { final PanelConfig panelConfig = mConfigMap.get(panelId); if (panelConfig == null) { throw new IllegalStateException("Tried to access non-existing panel: " + panelId); } return panelConfig; } private boolean isCurrentDefaultPanel(PanelConfig panelConfig) { if (mDefaultPanel == null) { return false; } return mDefaultPanel.equals(panelConfig); } private void findNewDefault() { // Pick the first panel that is neither disabled nor currently // set as default. for (PanelConfig panelConfig : makeOrderedCopy(false)) { if (!panelConfig.isDefault() && !panelConfig.isDisabled()) { setDefault(panelConfig.getId()); return; } } mDefaultPanel = null; } /** * Makes an ordered list of PanelConfigs that can be references * or deep copied objects. * * @param deepCopy true to make deep-copied objects * @return ordered List of PanelConfigs */ private List makeOrderedCopy(boolean deepCopy) { final List copiedList = new ArrayList(mConfigOrder.size()); for (String panelId : mConfigOrder) { PanelConfig panelConfig = mConfigMap.get(panelId); if (deepCopy) { panelConfig = new PanelConfig(panelConfig); } copiedList.add(panelConfig); } return copiedList; } private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) { if (panelConfig.isDisabled() == disabled) { return; } panelConfig.setIsDisabled(disabled); mEnabledCount += (disabled ? -1 : 1); } /** * Gets the ID of the current default panel. */ public String getDefaultPanelId() { ThreadUtils.assertOnThread(mOriginalThread); if (mDefaultPanel == null) { return null; } return mDefaultPanel.getId(); } /** * Set a new default panel. * * @param panelId the ID of the new default panel. */ public void setDefault(String panelId) { ThreadUtils.assertOnThread(mOriginalThread); final PanelConfig panelConfig = getPanelOrThrow(panelId); if (isCurrentDefaultPanel(panelConfig)) { return; } if (mDefaultPanel != null) { mDefaultPanel.setIsDefault(false); } panelConfig.setIsDefault(true); setPanelIsDisabled(panelConfig, false); mDefaultPanel = panelConfig; mHasChanged = true; } /** * Toggles disabled state for a panel. * * @param panelId the ID of the target panel. * @param disabled true to disable the panel. */ public void setDisabled(String panelId, boolean disabled) { ThreadUtils.assertOnThread(mOriginalThread); final PanelConfig panelConfig = getPanelOrThrow(panelId); if (panelConfig.isDisabled() == disabled) { return; } setPanelIsDisabled(panelConfig, disabled); if (disabled) { if (isCurrentDefaultPanel(panelConfig)) { panelConfig.setIsDefault(false); findNewDefault(); } } else if (mEnabledCount == 1) { setDefault(panelId); } mHasChanged = true; } /** * Adds a new {@code PanelConfig}. It will do nothing if the * {@code Editor} already contains a panel with the same ID. * * @param panelConfig the {@code PanelConfig} instance to be added. * @return true if the item has been added. */ public boolean install(PanelConfig panelConfig) { ThreadUtils.assertOnThread(mOriginalThread); if (panelConfig == null) { throw new IllegalStateException("Can't install a null panel"); } if (!panelConfig.isDynamic()) { throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId()); } if (panelConfig.isDisabled()) { throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId()); } boolean installed = false; final String id = panelConfig.getId(); if (!mConfigMap.containsKey(id)) { mConfigMap.put(id, panelConfig); final int position = panelConfig.getPosition(); if (position < 0 || position >= mConfigOrder.size()) { mConfigOrder.add(id); } else { mConfigOrder.add(position, id); } mEnabledCount++; if (mEnabledCount == 1 || panelConfig.isDefault()) { setDefault(panelConfig.getId()); } installed = true; // Add an event to the queue if a new panel is successfully installed. mNotificationQueue.add(new Pair( "HomePanels:Installed", panelConfig.getId())); } mHasChanged = true; return installed; } /** * Removes an existing panel. * * @return true if the item has been removed. */ public boolean uninstall(String panelId) { ThreadUtils.assertOnThread(mOriginalThread); final PanelConfig panelConfig = mConfigMap.get(panelId); if (panelConfig == null) { return false; } if (!panelConfig.isDynamic()) { throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId()); } mConfigMap.remove(panelId); mConfigOrder.remove(panelId); if (!panelConfig.isDisabled()) { mEnabledCount--; } if (isCurrentDefaultPanel(panelConfig)) { findNewDefault(); } // Add an event to the queue if a panel is successfully uninstalled. mNotificationQueue.add(new Pair("HomePanels:Uninstalled", panelId)); mHasChanged = true; return true; } /** * Moves panel associated with panelId to the specified position. * * @param panelId Id of panel * @param destIndex Destination position * @return true if move succeeded */ public boolean moveTo(String panelId, int destIndex) { ThreadUtils.assertOnThread(mOriginalThread); if (!mConfigOrder.contains(panelId)) { return false; } mConfigOrder.remove(panelId); mConfigOrder.add(destIndex, panelId); mHasChanged = true; return true; } /** * Replaces an existing panel with a new {@code PanelConfig} instance. * * @return true if the item has been updated. */ public boolean update(PanelConfig panelConfig) { ThreadUtils.assertOnThread(mOriginalThread); if (panelConfig == null) { throw new IllegalStateException("Can't update a null panel"); } boolean updated = false; final String id = panelConfig.getId(); if (mConfigMap.containsKey(id)) { final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig); // The disabled and default states can't never be // changed by an update operation. panelConfig.setIsDefault(oldPanelConfig.isDefault()); panelConfig.setIsDisabled(oldPanelConfig.isDisabled()); updated = true; } mHasChanged = true; return updated; } /** * Saves the current {@code Editor} state asynchronously in the * background thread. * * @return the resulting {@code State} instance. */ public State apply() { ThreadUtils.assertOnThread(mOriginalThread); // We're about to save the current state in the background thread // so we should use a deep copy of the PanelConfig instances to // avoid saving corrupted state. final State newConfigState = new State(mHomeConfig, makeOrderedCopy(true), isDefault()); // Copy the event queue to a new list, so that we only modify mNotificationQueue on // the original thread where it was created. final List> copiedQueue = mNotificationQueue; mNotificationQueue = new ArrayList<>(); ThreadUtils.getBackgroundHandler().post(new Runnable() { @Override public void run() { mHomeConfig.save(newConfigState); // Send pending events after the new config is saved. sendNotificationsToGecko(copiedQueue); } }); return newConfigState; } /** * Saves the current {@code Editor} state synchronously in the * current thread. * * @return the resulting {@code State} instance. */ public State commit() { ThreadUtils.assertOnThread(mOriginalThread); final State newConfigState = new State(mHomeConfig, makeOrderedCopy(false), isDefault()); // This is a synchronous blocking operation, hence no // need to deep copy the current PanelConfig instances. mHomeConfig.save(newConfigState); // Send pending events after the new config is saved. sendNotificationsToGecko(mNotificationQueue); mNotificationQueue.clear(); return newConfigState; } /** * Returns whether the {@code Editor} represents the default * {@code HomeConfig} configuration without any unsaved changes. */ public boolean isDefault() { ThreadUtils.assertOnThread(mOriginalThread); return (!mHasChanged && mIsFromDefault); } public boolean isEmpty() { return mConfigMap.isEmpty(); } private void sendNotificationsToGecko(List> notifications) { for (Pair p : notifications) { GeckoAppShell.notifyObservers(p.first, p.second); } } private class EditorIterator implements Iterator { private final Iterator mOrderIterator; public EditorIterator() { mOrderIterator = mConfigOrder.iterator(); } @Override public boolean hasNext() { return mOrderIterator.hasNext(); } @Override public PanelConfig next() { final String panelId = mOrderIterator.next(); return mConfigMap.get(panelId); } @Override public void remove() { throw new UnsupportedOperationException("Can't 'remove' from on Editor iterator."); } } @Override public Iterator iterator() { ThreadUtils.assertOnThread(mOriginalThread); return new EditorIterator(); } } public interface OnReloadListener { public void onReload(); } public interface HomeConfigBackend { public State load(); public void save(State configState); public String getLocale(); public void setOnReloadListener(OnReloadListener listener); } // UUIDs used to create PanelConfigs for default built-in panels. These are // public because they can be used in "about:home?panel=UUID" query strings // to open specific panels without querying the active Home Panel // configuration. Because they don't consider the active configuration, it // is only sensible to do this for built-in panels (and not for dynamic // panels). private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e"; private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907"; private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8"; private static final String COMBINED_HISTORY_PANEL_ID = "4d716ce2-e063-486d-9e7c-b190d7b04dc6"; private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8"; private static final String REMOTE_TABS_PANEL_ID = "72429afd-8d8b-43d8-9189-14b779c563d0"; private static final String DEPRECATED_READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b"; private final HomeConfigBackend mBackend; public HomeConfig(HomeConfigBackend backend) { mBackend = backend; } public State load() { final State configState = mBackend.load(); configState.setHomeConfig(this); return configState; } public String getLocale() { return mBackend.getLocale(); } public void save(State configState) { mBackend.save(configState); } public void setOnReloadListener(OnReloadListener listener) { mBackend.setOnReloadListener(listener); } public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) { return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class)); } public static int getTitleResourceIdForBuiltinPanelType(PanelType panelType) { switch (panelType) { case TOP_SITES: return R.string.home_top_sites_title; case BOOKMARKS: case DEPRECATED_READING_LIST: return R.string.bookmarks_title; case DEPRECATED_HISTORY: case DEPRECATED_REMOTE_TABS: case DEPRECATED_RECENT_TABS: case COMBINED_HISTORY: return R.string.home_history_title; default: throw new IllegalArgumentException("Only for built-in panel types: " + panelType); } } public static String getIdForBuiltinPanelType(PanelType panelType) { switch (panelType) { case TOP_SITES: return TOP_SITES_PANEL_ID; case BOOKMARKS: return BOOKMARKS_PANEL_ID; case DEPRECATED_HISTORY: return HISTORY_PANEL_ID; case COMBINED_HISTORY: return COMBINED_HISTORY_PANEL_ID; case DEPRECATED_REMOTE_TABS: return REMOTE_TABS_PANEL_ID; case DEPRECATED_READING_LIST: return DEPRECATED_READING_LIST_PANEL_ID; case DEPRECATED_RECENT_TABS: return RECENT_TABS_PANEL_ID; default: throw new IllegalArgumentException("Only for built-in panel types: " + panelType); } } public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType, EnumSet flags) { final int titleId = getTitleResourceIdForBuiltinPanelType(panelType); final String id = getIdForBuiltinPanelType(panelType); return new PanelConfig(panelType, context.getString(titleId), id, flags); } public static HomeConfig getDefault(Context context) { return new HomeConfig(new HomeConfigPrefsBackend(context)); } }