diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 3ca252197e23..b3f175e09a5f 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -269,6 +269,8 @@ gbjar.sources += [ 'preferences/AlignRightLinkPreference.java', 'preferences/AndroidImport.java', 'preferences/AndroidImportPreference.java', + 'preferences/CustomListCategory.java', + 'preferences/CustomListPreference.java', 'preferences/FontSizePreference.java', 'preferences/GeckoPreferenceFragment.java', 'preferences/GeckoPreferences.java', diff --git a/mobile/android/base/preferences/CustomListCategory.java b/mobile/android/base/preferences/CustomListCategory.java new file mode 100644 index 000000000000..b3571c1811e5 --- /dev/null +++ b/mobile/android/base/preferences/CustomListCategory.java @@ -0,0 +1,69 @@ +/* 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.preferences; + +import android.content.Context; +import android.preference.PreferenceCategory; +import android.util.AttributeSet; + +public abstract class CustomListCategory extends PreferenceCategory { + protected CustomListPreference mDefaultReference; + + public CustomListCategory(Context context) { + super(context); + } + + public CustomListCategory(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomListCategory(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onAttachedToActivity() { + super.onAttachedToActivity(); + + setOrderingAsAdded(true); + } + + /** + * Set the default to some available list item. Used if the current default is removed or + * disabled. + */ + private void setFallbackDefault() { + if (getPreferenceCount() > 0) { + CustomListPreference aItem = (CustomListPreference) getPreference(0); + setDefault(aItem); + } + } + + /** + * Removes the given item from the set of available list items. + * This only updates the UI, so callers are responsible for persisting any state. + * + * @param item The given item to remove. + */ + public void uninstall(CustomListPreference item) { + removePreference(item); + if (item == mDefaultReference) { + // If the default is being deleted, set a new default. + setFallbackDefault(); + } + } + + /** + * Sets the given item as the current default. + * This only updates the UI, so callers are responsible for persisting any state. + * + * @param item The intended new default. + */ + public void setDefault(CustomListPreference item) { + mDefaultReference.setIsDefault(false); + item.setIsDefault(true); + mDefaultReference = item; + } +} diff --git a/mobile/android/base/preferences/CustomListPreference.java b/mobile/android/base/preferences/CustomListPreference.java new file mode 100644 index 000000000000..ba68859a5906 --- /dev/null +++ b/mobile/android/base/preferences/CustomListPreference.java @@ -0,0 +1,171 @@ +/* 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.preferences; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.preference.Preference; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.util.ThreadUtils; + +/** + * Represents an element in a CustomListCategory preference menu. + * This preference con display a dialog when clicked, and also supports + * being set as a default item within the preference list category. + */ + +public abstract class CustomListPreference extends Preference implements View.OnLongClickListener { + protected String LOGTAG = "CustomListPreference"; + + // Indices of the buttons of the Dialog. + public static final int INDEX_SET_DEFAULT_BUTTON = 0; + + // Dialog item labels. + protected String[] mDialogItems; + + // Dialog displayed when this element is tapped. + protected AlertDialog mDialog; + + // Cache label to avoid repeated use of the resource system. + public final String LABEL_IS_DEFAULT; + + protected boolean mIsDefault; + + // Enclosing parent category that contains this preference. + protected final CustomListCategory mParentCategory; + + /** + * Create a preference object to represent a list preference that is attached to + * a category. + * + * @param context The activity context we operate under. + * @param parentCategory The PreferenceCategory this object exists within. + */ + public CustomListPreference(Context context, CustomListCategory parentCategory) { + super(context); + + mParentCategory = parentCategory; + setLayoutResource(getPreferenceLayoutResource()); + + setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + CustomListPreference sPref = (CustomListPreference) preference; + sPref.showDialog(); + return true; + } + }); + + Resources res = getContext().getResources(); + + // Fetch this resource now, instead of every time we ever want to relabel a button. + LABEL_IS_DEFAULT = res.getString(R.string.pref_search_default); + + mDialogItems = getDialogStrings(); + } + + /** + * Returns the Android resource id for the layout. + */ + protected abstract int getPreferenceLayoutResource(); + + /** + * Set whether this object's UI should display this as the default item. To ensure proper ordering, + * this method should only be called after this Preference is added to the PreferenceCategory. + * @param isDefault Flag indicating if this represents the default list item. + */ + public void setIsDefault(boolean isDefault) { + mIsDefault = isDefault; + if (isDefault) { + setOrder(0); + setSummary(LABEL_IS_DEFAULT); + } else { + setOrder(1); + setSummary(""); + } + } + + /** + * Returns the strings to be displayed in the dialog. + */ + abstract protected String[] getDialogStrings(); + + /** + * Display a dialog for this preference, when the preference is clicked. + */ + public void showDialog() { + final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle(getTitle().toString()); + builder.setItems(mDialogItems, new DialogInterface.OnClickListener() { + // Forward relevant events to the container class for handling. + @Override + public void onClick(DialogInterface dialog, int indexClicked) { + hideDialog(); + onDialogIndexClicked(indexClicked); + } + }); + + configureDialogBuilder(builder); + + // We have to construct the dialog itself on the UI thread. + mDialog = builder.create(); + mDialog.setOnShowListener(new DialogInterface.OnShowListener() { + // Called when the dialog is shown (so we're finally able to manipulate button enabledness). + @Override + public void onShow(DialogInterface dialog) { + configureShownDialog(); + } + }); + mDialog.show(); + } + + /** + * (Optional) Configure the AlertDialog builder. + */ + protected void configureDialogBuilder(AlertDialog.Builder builder) { + return; + } + + abstract protected void onDialogIndexClicked(int index); + + /** + * Disables buttons in the shown AlertDialog as required. The button elements are not created + * until after show is called, so this method has to be called from the onShowListener above. + * @see this.showDialog + */ + protected void configureShownDialog() { + // If this is already the default list item, disable the button for setting this as the default. + final TextView defaultButton = (TextView) mDialog.getListView().getChildAt(INDEX_SET_DEFAULT_BUTTON); + if (mIsDefault) { + defaultButton.setEnabled(false); + + // Failure to unregister this listener leads to tapping the button dismissing the dialog + // without doing anything. + defaultButton.setOnClickListener(null); + } + } + + /** + * Hide the dialog we previously created, if any. + */ + public void hideDialog() { + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + } + + @Override + public boolean onLongClick(View view) { + // Show the preference dialog on long-press. + showDialog(); + return true; + } +} diff --git a/mobile/android/base/preferences/GeckoPreferences.java b/mobile/android/base/preferences/GeckoPreferences.java index b665159f1cb7..00f7a4fcab07 100644 --- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -153,8 +153,8 @@ public class GeckoPreferences final ListAdapter listAdapter = ((ListView) parent).getAdapter(); final Object listItem = listAdapter.getItem(position); - // Only SearchEnginePreference handles long clicks. - if (listItem instanceof SearchEnginePreference && listItem instanceof View.OnLongClickListener) { + // Only CustomListPreference handles long clicks. + if (listItem instanceof CustomListPreference && listItem instanceof View.OnLongClickListener) { final View.OnLongClickListener longClickListener = (View.OnLongClickListener) listItem; return longClickListener.onLongClick(view); } diff --git a/mobile/android/base/preferences/SearchEnginePreference.java b/mobile/android/base/preferences/SearchEnginePreference.java index d19ad32ec28c..3bdde15b9482 100644 --- a/mobile/android/base/preferences/SearchEnginePreference.java +++ b/mobile/android/base/preferences/SearchEnginePreference.java @@ -6,15 +6,12 @@ package org.mozilla.gecko.preferences; import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; -import android.preference.Preference; import android.text.SpannableString; import android.util.Log; import android.view.View; -import android.widget.TextView; import android.widget.Toast; import java.util.Iterator; @@ -32,66 +29,21 @@ import org.mozilla.gecko.widget.FaviconView; /** * Represents an element in the list of search engines on the preferences menu. */ -public class SearchEnginePreference extends Preference implements View.OnLongClickListener { - private static final String LOGTAG = "SearchEnginePreference"; +public class SearchEnginePreference extends CustomListPreference { + protected String LOGTAG = "SearchEnginePreference"; - // Indices in button array of the AlertDialog of the three buttons. - public static final int INDEX_SET_DEFAULT_BUTTON = 0; - public static final int INDEX_REMOVE_BUTTON = 1; - - // Cache label to avoid repeated use of the resource system. - public final String LABEL_IS_DEFAULT; - - // Specifies if this engine is configured as the default search engine. - private boolean mIsDefaultEngine; - - // Dialog element labels. - private String[] mDialogItems; - - // The popup displayed when this element is tapped. - private AlertDialog mDialog; - - private final SearchPreferenceCategory mParentCategory; + protected static final int INDEX_REMOVE_BUTTON = 1; // The icon to display in the prompt when clicked. private BitmapDrawable mPromptIcon; + // The bitmap backing the drawable above - needed separately for the FaviconView. private Bitmap mIconBitmap; private FaviconView mFaviconView; - /** - * Create a preference object to represent a search engine that is attached to category - * containingCategory. - * @param context The activity context we operate under. - * @param parentCategory The PreferenceCategory this object exists within. - * @see this.setSearchEngine - */ public SearchEnginePreference(Context context, SearchPreferenceCategory parentCategory) { - super(context); - mParentCategory = parentCategory; - - Resources res = getContext().getResources(); - - // Set the layout resource for this preference - includes a FaviconView. - setLayoutResource(R.layout.preference_search_engine); - - setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - SearchEnginePreference sPref = (SearchEnginePreference) preference; - sPref.showDialog(); - - return true; - } - }); - - // Fetch this resource now, instead of every time we ever want to relabel a button. - LABEL_IS_DEFAULT = res.getString(R.string.pref_search_default); - - // Set up default dialog items. - mDialogItems = new String[] { res.getString(R.string.pref_search_set_default), - res.getString(R.string.pref_search_remove) }; + super(context, parentCategory); } /** @@ -103,18 +55,72 @@ public class SearchEnginePreference extends Preference implements View.OnLongCli @Override protected void onBindView(View view) { super.onBindView(view); + // Set the icon in the FaviconView. mFaviconView = ((FaviconView) view.findViewById(R.id.search_engine_icon)); mFaviconView.updateAndScaleImage(mIconBitmap, getTitle().toString()); } @Override - public boolean onLongClick(View view) { - // Show the preference dialog on long-press. - showDialog(); - return true; + protected int getPreferenceLayoutResource() { + return R.layout.preference_search_engine; } + /** + * Returns the strings to be displayed in the dialog. + */ + @Override + protected String[] getDialogStrings() { + Resources res = getContext().getResources(); + return new String[] { res.getString(R.string.pref_search_set_default), + res.getString(R.string.pref_search_remove) }; + } + + @Override + public void showDialog() { + // If this is the last engine, then we are the default, and none of the options + // on this menu can do anything. + if (mParentCategory.getPreferenceCount() == 1) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getContext(), R.string.pref_search_last_toast, Toast.LENGTH_SHORT).show(); + } + }); + return; + } + + super.showDialog(); + } + + @Override + protected void configureDialogBuilder(AlertDialog.Builder builder) { + // Copy the icon from this object to the prompt we produce. We lazily create the drawable, + // as the user may not ever actually tap this object. + if (mPromptIcon == null && mIconBitmap != null) { + mPromptIcon = new BitmapDrawable(getContext().getResources(), mFaviconView.getBitmap()); + } + + builder.setIcon(mPromptIcon); + } + + @Override + protected void onDialogIndexClicked(int index) { + switch (index) { + case INDEX_SET_DEFAULT_BUTTON: + mParentCategory.setDefault(this); + break; + + case INDEX_REMOVE_BUTTON: + mParentCategory.uninstall(this); + break; + + default: + Log.w(LOGTAG, "Selected index out of range."); + break; + } + } + /** * Configure this Preference object from the Gecko search engine JSON object. * @param geckoEngineJSON The Gecko-formatted JSON object representing the search engine. @@ -184,121 +190,4 @@ public class SearchEnginePreference extends Preference implements View.OnLongCli Log.e(LOGTAG, "NullPointerException creating Bitmap. Most likely a zero-length bitmap.", e); } } - - /** - * Set if this object's UI should show that this is the default engine. To ensure proper ordering, - * this method should only be called after this Preference is added to the PreferenceCategory. - * @param isDefault Flag indicating if this represents the default engine. - */ - public void setIsDefaultEngine(boolean isDefault) { - mIsDefaultEngine = isDefault; - if (isDefault) { - setOrder(0); - setSummary(LABEL_IS_DEFAULT); - } else { - setOrder(1); - setSummary(""); - } - } - - /** - * Display the AlertDialog providing options to reconfigure this search engine. Sets an event - * listener to disable buttons in the dialog as appropriate after they have been constructed by - * Android. - * @see this.configureShownDialog - * @see this.hideDialog - */ - public void showDialog() { - // If we are the only engine left, then we are the default engine, and none of the options - // on this menu can do anything. - if (mParentCategory.getPreferenceCount() == 1) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getContext(), R.string.pref_search_last_toast, Toast.LENGTH_SHORT).show(); - } - }); - return; - } - - final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setTitle(getTitle().toString()); - builder.setItems(mDialogItems, new DialogInterface.OnClickListener() { - // Forward the various events that we care about to the container class for handling. - @Override - public void onClick(DialogInterface dialog, int indexClicked) { - hideDialog(); - switch (indexClicked) { - case INDEX_SET_DEFAULT_BUTTON: - mParentCategory.setDefault(SearchEnginePreference.this); - break; - case INDEX_REMOVE_BUTTON: - mParentCategory.uninstall(SearchEnginePreference.this); - break; - default: - Log.w(LOGTAG, "Selected index out of range."); - break; - } - } - }); - - // Copy the icon from this object to the prompt we produce. We lazily create the drawable, - // as the user may not ever actually tap this object. - if (mPromptIcon == null && mIconBitmap != null) { - mPromptIcon = new BitmapDrawable(getContext().getResources(), mFaviconView.getBitmap()); - } - builder.setIcon(mPromptIcon); - - // Icons are hidden until Bug 926711 is fixed. - //builder.setIcon(mPromptIcon); - - // We have to construct the dialog itself on the UI thread. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - mDialog = builder.create(); - mDialog.setOnShowListener(new DialogInterface.OnShowListener() { - // Called when the dialog is shown (so we're finally able to manipulate button enabledness). - @Override - public void onShow(DialogInterface dialog) { - configureShownDialog(); - } - }); - mDialog.show(); - } - }); - } - - /** - * Hide the dialog we previously created, if any. - */ - public void hideDialog() { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // Null check so we can chain engine-mutating methods up in SearchPreferenceCategory - // without consequence. - if (mDialog != null && mDialog.isShowing()) { - mDialog.dismiss(); - } - } - }); - } - - /** - * Disables buttons in the shown AlertDialog as required. The button elements are not created - * until after we call show, so this method has to be called from the onShowListener above. - * @see this.showDialog - */ - private void configureShownDialog() { - // If we are the default engine, disable the "Set as default" button. - final TextView defaultButton = (TextView) mDialog.getListView().getChildAt(INDEX_SET_DEFAULT_BUTTON); - // Disable "Set as default" button if we are already the default. - if (mIsDefaultEngine) { - defaultButton.setEnabled(false); - // Failure to unregister this listener leads to tapping the button dismissing the dialog - // without doing anything. - defaultButton.setOnClickListener(null); - } - } } diff --git a/mobile/android/base/preferences/SearchPreferenceCategory.java b/mobile/android/base/preferences/SearchPreferenceCategory.java index 7ab630584471..121f1283a626 100644 --- a/mobile/android/base/preferences/SearchPreferenceCategory.java +++ b/mobile/android/base/preferences/SearchPreferenceCategory.java @@ -6,43 +6,36 @@ package org.mozilla.gecko.preferences; import android.content.Context; import android.preference.Preference; -import android.preference.PreferenceCategory; import android.util.AttributeSet; import android.util.Log; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; + import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.util.GeckoEventListener; -public class SearchPreferenceCategory extends PreferenceCategory implements GeckoEventListener { +public class SearchPreferenceCategory extends CustomListCategory implements GeckoEventListener { public static final String LOGTAG = "SearchPrefCategory"; - private SearchEnginePreference mDefaultEngineReference; - - // These seemingly redundant constructors are mandated by the Android system, else it fails to - // inflate this object. - - public SearchPreferenceCategory(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SearchPreferenceCategory(Context context) { + super(context); } public SearchPreferenceCategory(Context context, AttributeSet attrs) { super(context, attrs); } - public SearchPreferenceCategory(Context context) { - super(context); + public SearchPreferenceCategory(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); } @Override protected void onAttachedToActivity() { super.onAttachedToActivity(); - // Ensures default engine remains at top of list. - setOrderingAsAdded(true); - // Register for SearchEngines messages and request list of search engines from Gecko. GeckoAppShell.registerEventListener("SearchEngines:Data", this); GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null)); @@ -53,6 +46,20 @@ public class SearchPreferenceCategory extends PreferenceCategory implements Geck GeckoAppShell.unregisterEventListener("SearchEngines:Data", this); } + @Override + public void setDefault(CustomListPreference item) { + super.setDefault(item); + + sendGeckoEngineEvent("SearchEngines:SetDefault", item.getTitle().toString()); + } + + @Override + public void uninstall(CustomListPreference item) { + super.uninstall(item); + + sendGeckoEngineEvent("SearchEngines:Remove", item.getTitle().toString()); + } + @Override public void handleMessage(String event, final JSONObject data) { if (event.equals("SearchEngines:Data")) { @@ -93,8 +100,8 @@ public class SearchPreferenceCategory extends PreferenceCategory implements Geck // We set this here, not in setSearchEngineFromJSON, because it allows us to // keep a reference to the default engine to use when the AlertDialog // callbacks are used. - enginePreference.setIsDefaultEngine(true); - mDefaultEngineReference = enginePreference; + enginePreference.setIsDefault(true); + mDefaultReference = enginePreference; } } catch (JSONException e) { Log.e(LOGTAG, "JSONException parsing engine at index " + i, e); @@ -103,58 +110,19 @@ public class SearchPreferenceCategory extends PreferenceCategory implements Geck } } - /** - * Set the default engine to any available engine. Used if the current default is removed or - * disabled. - */ - private void setFallbackDefaultEngine() { - if (getPreferenceCount() > 0) { - SearchEnginePreference aEngine = (SearchEnginePreference) getPreference(0); - setDefault(aEngine); - } - } - /** * Helper method to send a particular event string to Gecko with an associated engine name. * @param event The type of event to send. * @param engine The engine to which the event relates. */ - private void sendGeckoEngineEvent(String event, SearchEnginePreference engine) { + private void sendGeckoEngineEvent(String event, String engineName) { JSONObject json = new JSONObject(); try { - json.put("engine", engine.getTitle()); + json.put("engine", engineName); } catch (JSONException e) { Log.e(LOGTAG, "JSONException creating search engine configuration change message for Gecko.", e); return; } GeckoAppShell.notifyGeckoOfEvent(GeckoEvent.createBroadcastEvent(event, json.toString())); } - - // Methods called by tapping items on the submenus for each search engine are below. - - /** - * Removes the given engine from the set of available engines. - * @param engine The engine to remove. - */ - public void uninstall(SearchEnginePreference engine) { - removePreference(engine); - if (engine == mDefaultEngineReference) { - // If they're deleting their default engine, get them a new default engine. - setFallbackDefaultEngine(); - } - - sendGeckoEngineEvent("SearchEngines:Remove", engine); - } - - /** - * Sets the given engine as the current default engine. - * @param engine The intended new default engine. - */ - public void setDefault(SearchEnginePreference engine) { - engine.setIsDefaultEngine(true); - mDefaultEngineReference.setIsDefaultEngine(false); - mDefaultEngineReference = engine; - - sendGeckoEngineEvent("SearchEngines:SetDefault", engine); - } }