Bug 942875 - Part 1: Refactor Search Settings. r=lucasr

This commit is contained in:
Chenxia Liu 2014-01-22 10:10:51 -08:00
parent 52eb7c323f
commit 7944986e4c
6 changed files with 332 additions and 233 deletions

View File

@ -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',

View File

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

View File

@ -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 <code>CustomListCategory</code> 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;
}
}

View File

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

View File

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

View File

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