pt 1 in reviving the android build (copied from pm 28a1, wish me luck)

This commit is contained in:
wuggy 2026-04-08 15:43:25 -07:00
commit d7788a6d6d
4249 changed files with 468189 additions and 0 deletions

View file

@ -0,0 +1,24 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import org.mozilla.gecko.R;
import android.content.Context;
import android.util.AttributeSet;
class AlignRightLinkPreference extends LinkPreference {
public AlignRightLinkPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_rightalign_icon);
}
public AlignRightLinkPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setLayoutResource(R.layout.preference_rightalign_icon);
}
}

View file

@ -0,0 +1,230 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import android.content.ContentValues;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.IconsHelper;
import org.mozilla.gecko.icons.storage.DiskStorage;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
public class AndroidImport implements Runnable {
/**
* The Android M SDK removed several fields and methods from android.provider.Browser. This class is used as a
* replacement to support building with the new SDK but at the same time still use these fields on lower Android
* versions.
*/
private static class LegacyBrowserProvider {
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");
// Incomplete: This are just the fields we currently use in our code base
public static class BookmarkColumns implements BaseColumns {
public static final String URL = "url";
public static final String VISITS = "visits";
public static final String DATE = "date";
public static final String BOOKMARK = "bookmark";
public static final String TITLE = "title";
public static final String CREATED = "created";
public static final String FAVICON = "favicon";
}
}
public static final Uri SAMSUNG_BOOKMARKS_URI = Uri.parse("content://com.sec.android.app.sbrowser.browser/bookmarks");
public static final Uri SAMSUNG_HISTORY_URI = Uri.parse("content://com.sec.android.app.sbrowser.browser/history");
public static final String SAMSUNG_MANUFACTURER = "samsung";
private static final String LOGTAG = "AndroidImport";
private final Context mContext;
private final Runnable mOnDoneRunnable;
private final ArrayList<ContentProviderOperation> mOperations;
private final ContentResolver mCr;
private final LocalBrowserDB mDB;
private final boolean mImportBookmarks;
private final boolean mImportHistory;
public AndroidImport(Context context, Runnable onDoneRunnable,
boolean doBookmarks, boolean doHistory) {
mContext = context;
mOnDoneRunnable = onDoneRunnable;
mOperations = new ArrayList<ContentProviderOperation>();
mCr = mContext.getContentResolver();
mDB = new LocalBrowserDB(GeckoProfile.get(context).getName());
mImportBookmarks = doBookmarks;
mImportHistory = doHistory;
}
public void mergeBookmarks() {
Cursor cursor = null;
try {
cursor = query(LegacyBrowserProvider.BOOKMARKS_URI,
SAMSUNG_BOOKMARKS_URI,
LegacyBrowserProvider.BookmarkColumns.BOOKMARK + " = 1");
if (cursor != null) {
final int faviconCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.FAVICON);
final int titleCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.TITLE);
final int urlCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.URL);
// http://code.google.com/p/android/issues/detail?id=17969
final int createCol = cursor.getColumnIndex(LegacyBrowserProvider.BookmarkColumns.CREATED);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
String url = cursor.getString(urlCol);
String title = cursor.getString(titleCol);
long created;
if (createCol >= 0) {
created = cursor.getLong(createCol);
} else {
created = System.currentTimeMillis();
}
// Need to set it to the current time so Sync picks it up.
long modified = System.currentTimeMillis();
byte[] data = cursor.getBlob(faviconCol);
mDB.updateBookmarkInBatch(mCr, mOperations,
url, title, null, -1,
created, modified,
BrowserContract.Bookmarks.DEFAULT_POSITION,
null, Bookmarks.TYPE_BOOKMARK);
if (data != null) {
storeBitmap(data, url);
}
cursor.moveToNext();
}
}
} finally {
if (cursor != null)
cursor.close();
}
flushBatchOperations();
}
public void mergeHistory() {
ArrayList<ContentValues> visitsToSynthesize = new ArrayList<>();
Cursor cursor = null;
try {
cursor = query (LegacyBrowserProvider.BOOKMARKS_URI,
SAMSUNG_HISTORY_URI,
LegacyBrowserProvider.BookmarkColumns.BOOKMARK + " = 0 AND " +
LegacyBrowserProvider.BookmarkColumns.VISITS + " > 0");
if (cursor != null) {
final int dateCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.DATE);
final int faviconCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.FAVICON);
final int titleCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.TITLE);
final int urlCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.URL);
final int visitsCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.VISITS);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
String url = cursor.getString(urlCol);
String title = cursor.getString(titleCol);
long date = cursor.getLong(dateCol);
int visits = cursor.getInt(visitsCol);
byte[] data = cursor.getBlob(faviconCol);
mDB.updateHistoryInBatch(mCr, mOperations, url, title, date, visits);
if (data != null) {
storeBitmap(data, url);
}
ContentValues visitData = new ContentValues();
visitData.put(LocalBrowserDB.HISTORY_VISITS_DATE, date);
visitData.put(LocalBrowserDB.HISTORY_VISITS_URL, url);
visitData.put(LocalBrowserDB.HISTORY_VISITS_COUNT, visits);
visitsToSynthesize.add(visitData);
cursor.moveToNext();
}
}
} finally {
if (cursor != null)
cursor.close();
}
flushBatchOperations();
// Now that we have flushed history records, we need to synthesize individual visits. We have
// gathered information about all of the visits we need to synthesize into visitsForSynthesis.
mDB.insertVisitsFromImportHistoryInBatch(mCr, mOperations, visitsToSynthesize);
flushBatchOperations();
}
private void storeBitmap(byte[] data, String url) {
if (TextUtils.isEmpty(url) || data == null) {
return;
}
final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bitmap == null) {
return;
}
final String iconUrl = IconsHelper.guessDefaultFaviconURL(url);
if (iconUrl == null) {
return;
}
final DiskStorage storage = DiskStorage.get(mContext);
storage.putIcon(url, bitmap);
storage.putMapping(url, iconUrl);
}
protected Cursor query(Uri mainUri, Uri fallbackUri, String condition) {
final Cursor cursor = mCr.query(mainUri, null, condition, null, null);
if (Build.MANUFACTURER.equals(SAMSUNG_MANUFACTURER) && (cursor == null || cursor.getCount() == 0)) {
if (cursor != null) {
cursor.close();
}
return mCr.query(fallbackUri, null, null, null, null);
}
return cursor;
}
protected void flushBatchOperations() {
Log.d(LOGTAG, "Flushing " + mOperations.size() + " DB operations");
try {
// We don't really care for the results, this is best-effort.
mCr.applyBatch(BrowserContract.AUTHORITY, mOperations);
} catch (RemoteException e) {
Log.e(LOGTAG, "Remote exception while updating db: ", e);
} catch (OperationApplicationException e) {
// Bug 716729 means this happens even in normal circumstances
Log.d(LOGTAG, "Error while applying database updates: ", e);
}
mOperations.clear();
}
@Override
public void run() {
if (mImportBookmarks) {
mergeBookmarks();
}
if (mImportHistory) {
mergeHistory();
}
mOnDoneRunnable.run();
}
}

View file

@ -0,0 +1,112 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.R;
import org.mozilla.gecko.restrictions.Restrictable;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.util.ThreadUtils;
import java.util.Set;
import android.app.ProgressDialog;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
class AndroidImportPreference extends MultiPrefMultiChoicePreference {
private static final String LOGTAG = "AndroidImport";
public static final String PREF_KEY = "android.not_a_preference.import_android";
private static final String PREF_KEY_PREFIX = "import_android.data.";
private final Context mContext;
public static class Handler implements GeckoPreferences.PrefHandler {
public boolean setupPref(Context context, Preference pref) {
// Feature disabled on devices running Android M+ (Bug 1183559)
return Versions.preMarshmallow && Restrictions.isAllowed(context, Restrictable.IMPORT_SETTINGS);
}
public void onChange(Context context, Preference pref, Object newValue) { }
}
public AndroidImportPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (!positiveResult)
return;
boolean bookmarksChecked = false;
boolean historyChecked = false;
Set<String> values = getValues();
for (String value : values) {
// Import checkbox values are stored in Android prefs to
// remember their check states. The key names are import_android.data.X
String key = value.substring(PREF_KEY_PREFIX.length());
if ("bookmarks".equals(key)) {
bookmarksChecked = true;
} else if ("history".equals(key)) {
historyChecked = true;
}
}
runImport(bookmarksChecked, historyChecked);
}
protected void runImport(final boolean doBookmarks, final boolean doHistory) {
Log.i(LOGTAG, "Importing Android history/bookmarks");
if (!doBookmarks && !doHistory) {
return;
}
final String dialogTitle;
if (doBookmarks && doHistory) {
dialogTitle = mContext.getString(R.string.bookmarkhistory_import_both);
} else if (doBookmarks) {
dialogTitle = mContext.getString(R.string.bookmarkhistory_import_bookmarks);
} else {
dialogTitle = mContext.getString(R.string.bookmarkhistory_import_history);
}
final ProgressDialog dialog =
ProgressDialog.show(mContext,
dialogTitle,
mContext.getString(R.string.bookmarkhistory_import_wait),
true);
final Runnable stopCallback = new Runnable() {
@Override
public void run() {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
dialog.dismiss();
}
});
}
};
ThreadUtils.postToBackgroundThread(
// Constructing AndroidImport may need finding the profile,
// which hits disk, so it needs to go into a Runnable too.
new Runnable() {
@Override
public void run() {
new AndroidImport(mContext, stopCallback, doBookmarks, doHistory).run();
}
}
);
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.preferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
* to be used with AppCompat.
*
* This technique can be used with an {@link android.app.Activity} class, not just
* {@link android.preference.PreferenceActivity}.
*
* This class was directly imported (without any modifications) from Android SDK examples, at:
* https://android.googlesource.com/platform/development/+/master/samples/Support7Demos/src/com/example/android/supportv7/app/AppCompatPreferenceActivity.java
*/
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
public ActionBar getSupportActionBar() {
return getDelegate().getSupportActionBar();
}
public void setSupportActionBar(@Nullable Toolbar toolbar) {
getDelegate().setSupportActionBar(toolbar);
}
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().addContentView(view, params);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
public void invalidateOptionsMenu() {
getDelegate().invalidateOptionsMenu();
}
private AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, null);
}
return mDelegate;
}
}

View file

@ -0,0 +1,37 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import java.util.HashSet;
import java.util.Set;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.util.PrefUtils;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.Preference;
public class ClearOnShutdownPref implements GeckoPreferences.PrefHandler {
public static final String PREF = GeckoPreferences.NON_PREF_PREFIX + "history.clear_on_exit";
@Override
public boolean setupPref(Context context, Preference pref) {
// The pref is initialized asynchronously. Read the pref explicitly
// here to make sure we have the data.
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
final Set<String> clearItems = PrefUtils.getStringSet(prefs, PREF, new HashSet<String>());
((ListCheckboxPreference) pref).setChecked(clearItems.size() > 0);
return true;
}
@Override
@SuppressWarnings("unchecked")
public void onChange(Context context, Preference pref, Object newValue) {
final Set<String> vals = (Set<String>) newValue;
((ListCheckboxPreference) pref).setChecked(vals.size() > 0);
}
}

View file

@ -0,0 +1,44 @@
/* 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.CheckBoxPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Represents a Checkbox element in a preference menu.
* The title of the Checkbox can be larger than the view.
* In this case, it will be displayed in 2 or more lines.
* The default behavior of the class CheckBoxPreference
* doesn't wrap the title.
*/
public class CustomCheckBoxPreference extends CheckBoxPreference {
public CustomCheckBoxPreference(Context context) {
super(context);
}
public CustomCheckBoxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomCheckBoxPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
final TextView title = (TextView) view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
title.setEllipsize(null);
}
}
}

View file

@ -0,0 +1,72 @@
/* 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.
*/
protected 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) {
if (mDefaultReference != null) {
mDefaultReference.setIsDefault(false);
}
item.setIsDefault(true);
mDefaultReference = item;
}
}

View file

@ -0,0 +1,182 @@
/* 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 org.mozilla.gecko.R;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.preference.Preference;
import android.view.View;
import android.widget.TextView;
/**
* 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.
private String[] mDialogItems;
// Dialog displayed when this element is tapped.
protected AlertDialog mDialog;
// Cache label to avoid repeated use of the resource system.
protected final String LABEL_IS_DEFAULT;
protected final String LABEL_SET_AS_DEFAULT;
protected final String LABEL_REMOVE;
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 these strings now, instead of every time we ever want to relabel a button.
LABEL_IS_DEFAULT = res.getString(R.string.pref_default);
LABEL_SET_AS_DEFAULT = res.getString(R.string.pref_dialog_set_default);
LABEL_REMOVE = res.getString(R.string.pref_dialog_remove);
}
/**
* 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.
* Note: This must be called from the UI thread because it touches the view hierarchy.
*
* 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("");
}
}
private String[] getCachedDialogItems() {
if (mDialogItems == null) {
mDialogItems = createDialogItems();
}
return mDialogItems;
}
/**
* Returns the strings to be displayed in the dialog.
*/
abstract protected String[] createDialogItems();
/**
* 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(getCachedDialogItems(), 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

@ -0,0 +1,61 @@
/* -*- 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.preferences;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.distribution.Distribution;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Iterator;
public class DistroSharedPrefsImport {
public static final String LOGTAG = DistroSharedPrefsImport.class.getSimpleName();
public static void importPreferences(final Context context, final Distribution distribution) {
if (distribution == null) {
return;
}
final JSONObject preferences = distribution.getAndroidPreferences();
if (preferences.length() == 0) {
return;
}
final Iterator<?> keys = preferences.keys();
final SharedPreferences.Editor sharedPreferences = GeckoSharedPrefs.forProfile(context).edit();
while (keys.hasNext()) {
final String key = (String) keys.next();
final Object value;
try {
value = preferences.get(key);
} catch (JSONException e) {
Log.e(LOGTAG, "Unable to completely process Android Preferences JSON.", e);
continue;
}
// We currently don't support Float preferences.
if (value instanceof String) {
sharedPreferences.putString(GeckoPreferences.NON_PREF_PREFIX + key, (String) value);
} else if (value instanceof Boolean) {
sharedPreferences.putBoolean(GeckoPreferences.NON_PREF_PREFIX + key, (boolean) value);
} else if (value instanceof Integer) {
sharedPreferences.putInt(GeckoPreferences.NON_PREF_PREFIX + key, (int) value);
} else if (value instanceof Long) {
sharedPreferences.putLong(GeckoPreferences.NON_PREF_PREFIX + key, (long) value);
} else {
Log.d(LOGTAG, "Unknown preference value type whilst importing android preferences from distro file.");
}
}
sharedPreferences.apply();
}
}

View file

@ -0,0 +1,192 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import org.mozilla.gecko.R;
import android.app.AlertDialog;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
import java.util.HashMap;
class FontSizePreference extends DialogPreference {
private static final String LOGTAG = "FontSizePreference";
private static final int TWIP_TO_PT_RATIO = 20; // 20 twip = 1 point.
private static final int PREVIEW_FONT_SIZE_UNIT = TypedValue.COMPLEX_UNIT_PT;
private static final int DEFAULT_FONT_INDEX = 2;
private final Context mContext;
/** Container for mPreviewFontView to allow for scrollable padding at the top of the view. */
private ScrollView mScrollingContainer;
private TextView mPreviewFontView;
private Button mIncreaseFontButton;
private Button mDecreaseFontButton;
private final String[] mFontTwipValues;
private final String[] mFontSizeNames; // Ex: "Small".
/** Index into the above arrays for the saved preference value (from Gecko). */
private int mSavedFontIndex = DEFAULT_FONT_INDEX;
/** Index into the above arrays for the currently displayed font size (the preview). */
private int mPreviewFontIndex = mSavedFontIndex;
private final HashMap<String, Integer> mFontTwipToIndexMap;
public FontSizePreference(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
final Resources res = mContext.getResources();
mFontTwipValues = res.getStringArray(R.array.pref_font_size_values);
mFontSizeNames = res.getStringArray(R.array.pref_font_size_entries);
mFontTwipToIndexMap = new HashMap<String, Integer>();
for (int i = 0; i < mFontTwipValues.length; ++i) {
mFontTwipToIndexMap.put(mFontTwipValues[i], i);
}
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
final LayoutInflater inflater =
(LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View dialogView = inflater.inflate(R.layout.font_size_preference, null);
initInternalViews(dialogView);
updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
builder.setTitle(null);
builder.setView(dialogView);
}
/** Saves relevant views to instance variables and initializes their settings. */
private void initInternalViews(View dialogView) {
mScrollingContainer = (ScrollView) dialogView.findViewById(R.id.scrolling_container);
// Background cannot be set in XML (see bug 783597 - TODO: Change this to XML when bug is fixed).
mScrollingContainer.setBackgroundColor(Color.WHITE);
mPreviewFontView = (TextView) dialogView.findViewById(R.id.preview);
mDecreaseFontButton = (Button) dialogView.findViewById(R.id.decrease_preview_font_button);
mIncreaseFontButton = (Button) dialogView.findViewById(R.id.increase_preview_font_button);
setButtonState(mPreviewFontIndex);
mDecreaseFontButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPreviewFontIndex = Math.max(mPreviewFontIndex - 1, 0);
updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
mIncreaseFontButton.setEnabled(true);
// If we reached the minimum index, disable the button.
if (mPreviewFontIndex == 0) {
mDecreaseFontButton.setEnabled(false);
}
}
});
mIncreaseFontButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPreviewFontIndex = Math.min(mPreviewFontIndex + 1, mFontTwipValues.length - 1);
updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
mDecreaseFontButton.setEnabled(true);
// If we reached the maximum index, disable the button.
if (mPreviewFontIndex == mFontTwipValues.length - 1) {
mIncreaseFontButton.setEnabled(false);
}
}
});
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (!positiveResult) {
mPreviewFontIndex = mSavedFontIndex;
return;
}
mSavedFontIndex = mPreviewFontIndex;
final String twipVal = mFontTwipValues[mSavedFontIndex];
final OnPreferenceChangeListener prefChangeListener = getOnPreferenceChangeListener();
if (prefChangeListener == null) {
Log.e(LOGTAG, "PreferenceChangeListener is null. FontSizePreference will not be saved to Gecko.");
return;
}
prefChangeListener.onPreferenceChange(this, twipVal);
}
/**
* Finds the index of the given twip value and sets it as the saved preference value. Also the
* current preview text size to the given value. Does not update the mPreviewFontView text size.
*/
protected void setSavedFontSize(String twip) {
final Integer index = mFontTwipToIndexMap.get(twip);
if (index != null) {
mSavedFontIndex = index;
mPreviewFontIndex = mSavedFontIndex;
return;
}
resetSavedFontSizeToDefault();
Log.e(LOGTAG, "setSavedFontSize: Given font size does not exist in twip values map. Reverted to default font size.");
}
/**
* Updates the mPreviewFontView to the given text size, resets the container's scroll to the top
* left, and invalidates the view. Does not update the font indices.
*/
private void updatePreviewFontSize(String twip) {
float pt = convertTwipStrToPT(twip);
// Android will not render a font size of 0 pt but for Gecko, 0 twip turns off font
// inflation. Thus we special case 0 twip to display a renderable font size.
if (pt == 0) {
// Android adds an inexplicable extra margin on the smallest font size so to get around
// this, we reinflate the view.
ViewGroup parentView = (ViewGroup) mScrollingContainer.getParent();
parentView.removeAllViews();
final LayoutInflater inflater =
(LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View dialogView = inflater.inflate(R.layout.font_size_preference, parentView);
initInternalViews(dialogView);
mPreviewFontView.setTextSize(PREVIEW_FONT_SIZE_UNIT, 1);
} else {
mPreviewFontView.setTextSize(PREVIEW_FONT_SIZE_UNIT, pt);
}
mScrollingContainer.scrollTo(0, 0);
}
/**
* Resets the font indices to the default value. Does not update the mPreviewFontView text size.
*/
private void resetSavedFontSizeToDefault() {
mSavedFontIndex = DEFAULT_FONT_INDEX;
mPreviewFontIndex = mSavedFontIndex;
}
private void setButtonState(int index) {
if (index == 0) {
mDecreaseFontButton.setEnabled(false);
} else if (index == mFontTwipValues.length - 1) {
mIncreaseFontButton.setEnabled(false);
}
}
/**
* Returns the name of the font size (ex: "Small") at the currently saved preference value.
*/
protected String getSavedFontSizeName() {
return mFontSizeNames[mSavedFontIndex];
}
private float convertTwipStrToPT(String twip) {
return Float.parseFloat(twip) / TWIP_TO_PT_RATIO;
}
}

View file

@ -0,0 +1,296 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import java.util.Locale;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.LocaleManager;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.TelemetryContract.Method;
import org.mozilla.gecko.fxa.AccountLoader;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import android.accounts.Account;
import android.app.ActionBar;
import android.app.Activity;
import android.app.LoaderManager;
import android.content.Context;
import android.content.Loader;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import com.squareup.leakcanary.RefWatcher;
/* A simple implementation of PreferenceFragment for large screen devices
* This will strip category headers (so that they aren't shown to the user twice)
* as well as initializing Gecko prefs when a fragment is shown.
*/
public class GeckoPreferenceFragment extends PreferenceFragment {
public static final int ACCOUNT_LOADER_ID = 1;
private AccountLoaderCallbacks accountLoaderCallbacks;
private SyncPreference syncPreference;
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
final Activity context = getActivity();
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
final Locale changed = localeManager.onSystemConfigurationChanged(context, getResources(), newConfig, lastLocale);
if (changed != null) {
applyLocale(changed);
}
}
private static final String LOGTAG = "GeckoPreferenceFragment";
private PrefsHelper.PrefHandler mPrefsRequest;
private Locale lastLocale = Locale.getDefault();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Write prefs to our custom GeckoSharedPrefs file.
getPreferenceManager().setSharedPreferencesName(GeckoSharedPrefs.APP_PREFS_NAME);
int res = getResource();
if (res == R.xml.preferences) {
Telemetry.startUISession(TelemetryContract.Session.SETTINGS);
} else {
final String resourceName = getArguments().getString("resource");
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.SETTINGS, resourceName);
}
// Display a menu for Search preferences.
if (res == R.xml.preferences_search) {
setHasOptionsMenu(true);
}
addPreferencesFromResource(res);
PreferenceScreen screen = getPreferenceScreen();
setPreferenceScreen(screen);
mPrefsRequest = ((GeckoPreferences)getActivity()).setupPreferences(screen);
syncPreference = (SyncPreference) findPreference(GeckoPreferences.PREFS_SYNC);
}
/**
* Return the title to use for this preference fragment.
*
* We only return titles for the preference screens that are
* launched directly, and thus might need to be redisplayed.
*
* This method sets the title that you see on non-multi-pane devices.
*/
private String getTitle() {
final int res = getResource();
if (res == R.xml.preferences) {
return getString(R.string.settings_title);
}
// We can launch this category from the Data Reporting notification.
if (res == R.xml.preferences_privacy) {
return getString(R.string.pref_category_privacy_short);
}
// We can launch this category from the the magnifying glass in the quick search bar.
if (res == R.xml.preferences_search) {
return getString(R.string.pref_category_search);
}
// Launched as action from content notifications.
if (res == R.xml.preferences_notifications) {
return getString(R.string.pref_category_notifications);
}
return null;
}
/**
* Return the header id for this preference fragment. This allows
* us to select the correct header when launching a preference
* screen directly.
*
* We only return titles for the preference screens that are
* launched directly.
*/
private int getHeader() {
final int res = getResource();
if (res == R.xml.preferences) {
return R.id.pref_header_general;
}
// We can launch this category from the Data Reporting notification.
if (res == R.xml.preferences_privacy) {
return R.id.pref_header_privacy;
}
// We can launch this category from the the magnifying glass in the quick search bar.
if (res == R.xml.preferences_search) {
return R.id.pref_header_search;
}
// Launched as action from content notifications.
if (res == R.xml.preferences_notifications) {
return R.id.pref_header_notifications;
}
return -1;
}
private void updateTitle() {
final String newTitle = getTitle();
if (newTitle == null) {
Log.d(LOGTAG, "No new title to show.");
return;
}
final GeckoPreferences activity = (GeckoPreferences) getActivity();
if (activity.isMultiPane()) {
// In a multi-pane activity, the title is "Settings", and the action
// bar is along the top of the screen. We don't want to change those.
activity.showBreadCrumbs(newTitle, newTitle);
activity.switchToHeader(getHeader());
return;
}
Log.v(LOGTAG, "Setting activity title to " + newTitle);
activity.setTitle(newTitle);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
accountLoaderCallbacks = new AccountLoaderCallbacks();
getLoaderManager().initLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks);
}
@Override
public void onResume() {
// This is a little delicate. Ensure that you do nothing prior to
// super.onResume that you wouldn't do in onCreate.
applyLocale(Locale.getDefault());
super.onResume();
// Force reload as the account may have been deleted while the app was in background.
getLoaderManager().restartLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks);
}
private void applyLocale(final Locale currentLocale) {
final Context context = getActivity().getApplicationContext();
BrowserLocaleManager.getInstance().updateConfiguration(context, currentLocale);
if (!currentLocale.equals(lastLocale)) {
// Locales differ. Let's redisplay.
Log.d(LOGTAG, "Locale changed: " + currentLocale);
this.lastLocale = currentLocale;
// Rebuild the list to reflect the current locale.
getPreferenceScreen().removeAll();
addPreferencesFromResource(getResource());
}
// Fix the parent title regardless.
updateTitle();
}
/*
* Get the resource from Fragment arguments and return it.
*
* If no resource can be found, return the resource id of the default preference screen.
*/
private int getResource() {
int resid = 0;
final String resourceName = getArguments().getString("resource");
final Activity activity = getActivity();
if (resourceName != null) {
// Fetch resource id by resource name.
final Resources resources = activity.getResources();
final String packageName = activity.getPackageName();
resid = resources.getIdentifier(resourceName, "xml", packageName);
}
if (resid == 0) {
// The resource was invalid. Use the default resource.
Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings.");
boolean isMultiPane = ((GeckoPreferences) activity).isMultiPane();
resid = isMultiPane ? R.xml.preferences_general_tablet : R.xml.preferences;
}
return resid;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.preferences_search_menu, menu);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mPrefsRequest != null) {
PrefsHelper.removeObserver(mPrefsRequest);
mPrefsRequest = null;
}
final int res = getResource();
if (res == R.xml.preferences) {
Telemetry.stopUISession(TelemetryContract.Session.SETTINGS);
}
GeckoApplication.watchReference(getActivity(), this);
}
private class AccountLoaderCallbacks implements LoaderManager.LoaderCallbacks<Account> {
@Override
public Loader<Account> onCreateLoader(int id, Bundle args) {
return new AccountLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Account> loader, Account account) {
if (syncPreference == null) {
return;
}
if (account == null) {
syncPreference.update(null);
return;
}
syncPreference.update(new AndroidFxAccount(getActivity(), account));
}
@Override
public void onLoaderReset(Loader<Account> loader) {
if (syncPreference != null) {
syncPreference.update(null);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import org.mozilla.gecko.Tabs;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
class LinkPreference extends Preference {
private String mUrl;
public LinkPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mUrl = attrs.getAttributeValue(null, "url");
}
public LinkPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mUrl = attrs.getAttributeValue(null, "url");
}
public void setUrl(String url) {
mUrl = url;
}
@Override
protected void onClick() {
Tabs.getInstance().loadUrlInTab(mUrl);
callChangeListener(mUrl);
}
}

View file

@ -0,0 +1,58 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Checkable;
import org.mozilla.gecko.R;
/**
* This preference shows a checkbox on its left hand side, but will show a menu when clicked.
* Its used for preferences like "Clear on Exit" that have a boolean on-off state, but that represent
* multiple boolean options inside.
**/
class ListCheckboxPreference extends MultiChoicePreference implements Checkable {
private static final String LOGTAG = "GeckoListCheckboxPreference";
private boolean checked;
public ListCheckboxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setWidgetLayoutResource(R.layout.preference_checkbox);
}
@Override
public boolean isChecked() {
return checked;
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
View checkboxView = view.findViewById(R.id.checkbox);
if (checkboxView instanceof Checkable) {
((Checkable) checkboxView).setChecked(checked);
}
}
@Override
public void setChecked(boolean checked) {
boolean changed = checked != this.checked;
this.checked = checked;
if (changed) {
notifyDependencyChange(shouldDisableDependents());
notifyChanged();
}
}
@Override
public void toggle() {
checked = !checked;
}
}

View file

@ -0,0 +1,316 @@
/* 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 java.nio.ByteBuffer;
import java.text.Collator;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.R;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.preference.ListPreference;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
public class LocaleListPreference extends ListPreference {
private static final String LOG_TAG = "GeckoLocaleList";
/**
* With thanks to <http://stackoverflow.com/a/22679283/22003> for the
* initial solution.
*
* This class encapsulates an approach to checking whether a script
* is usable on a device. We attempt to draw a character from the
* script (e.g., ). If the fonts on the device don't have the correct
* glyph, Android typically renders whitespace (rather than .notdef).
*
* Pass in part of the name of the locale in its local representation,
* and a whitespace character; this class performs the graphical comparison.
*
* See Bug 1023451 Comment 24 for extensive explanation.
*/
private static class CharacterValidator {
private static final int BITMAP_WIDTH = 32;
private static final int BITMAP_HEIGHT = 48;
private final Paint paint = new Paint();
private final byte[] missingCharacter;
public CharacterValidator(String missing) {
this.missingCharacter = getPixels(drawBitmap(missing));
}
private Bitmap drawBitmap(String text) {
Bitmap b = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ALPHA_8);
Canvas c = new Canvas(b);
c.drawText(text, 0, BITMAP_HEIGHT / 2, this.paint);
return b;
}
private static byte[] getPixels(final Bitmap b) {
final int byteCount;
if (Versions.feature19Plus) {
byteCount = b.getAllocationByteCount();
} else {
// Close enough for government work.
// Equivalent to getByteCount, but works on <12.
byteCount = b.getRowBytes() * b.getHeight();
}
final ByteBuffer buffer = ByteBuffer.allocate(byteCount);
try {
b.copyPixelsToBuffer(buffer);
} catch (RuntimeException e) {
// Android throws this if there's not enough space in the buffer.
// This should never occur, but if it does, we don't
// really care -- we probably don't need the entire image.
// This is awful. I apologize.
if ("Buffer not large enough for pixels".equals(e.getMessage())) {
return buffer.array();
}
throw e;
}
return buffer.array();
}
public boolean characterIsMissingInFont(String ch) {
byte[] rendered = getPixels(drawBitmap(ch));
return Arrays.equals(rendered, missingCharacter);
}
}
private volatile Locale entriesLocale;
private final CharacterValidator characterValidator;
public LocaleListPreference(Context context) {
this(context, null);
}
public LocaleListPreference(Context context, AttributeSet attributes) {
super(context, attributes);
// Thus far, missing glyphs are replaced by whitespace, not a box
// or other Unicode codepoint.
this.characterValidator = new CharacterValidator(" ");
buildList();
}
private static final class LocaleDescriptor implements Comparable<LocaleDescriptor> {
// We use Locale.US here to ensure a stable ordering of entries.
private static final Collator COLLATOR = Collator.getInstance(Locale.US);
public final String tag;
private final String nativeName;
public LocaleDescriptor(String tag) {
this(Locales.parseLocaleCode(tag), tag);
}
public LocaleDescriptor(Locale locale, String tag) {
this.tag = tag;
final String displayName = locale.getDisplayName(locale);
if (TextUtils.isEmpty(displayName)) {
// There's nothing sane we can do.
Log.w(LOG_TAG, "Display name is empty. Using " + locale.toString());
this.nativeName = locale.toString();
return;
}
// For now, uppercase the first character of LTR locale names.
// This is pretty much what Android does. This is a reasonable hack
// for Bug 1014602, but it won't generalize to all locales.
final byte directionality = Character.getDirectionality(displayName.charAt(0));
if (directionality == Character.DIRECTIONALITY_LEFT_TO_RIGHT) {
this.nativeName = displayName.substring(0, 1).toUpperCase(locale) +
displayName.substring(1);
return;
}
this.nativeName = displayName;
}
public String getTag() {
return this.tag;
}
public String getDisplayName() {
return this.nativeName;
}
@Override
public String toString() {
return this.nativeName;
}
@Override
public int compareTo(LocaleDescriptor another) {
// We sort by name, so we use Collator.
return COLLATOR.compare(this.nativeName, another.nativeName);
}
/**
* See Bug 1023451 Comment 10 for the research that led to
* this method.
*
* @return true if this locale can be used for displaying UI
* on this device without known issues.
*/
public boolean isUsable(CharacterValidator validator) {
if (Versions.preLollipop && this.tag.matches("[a-zA-Z]{3}.*")) {
// Earlier versions of Android can't load three-char locale code
// resources.
return false;
}
// Oh, for Java 7 switch statements.
if (this.tag.equals("bn-IN")) {
// Bengali sometimes has an English label if the Bengali script
// is missing. This prevents us from simply checking character
// rendering for bn-IN; we'll get a false positive for "B", not "".
//
// This doesn't seem to affect other Bengali-script locales
// (below), which always have a label in native script.
if (!this.nativeName.startsWith("বাংলা")) {
// We're on an Android version that doesn't even have
// characters to say . Definite failure.
return false;
}
}
// These locales use a script that is often unavailable
// on common Android devices. Make sure we can show them.
// See documentation for CharacterValidator.
// Note that bn-IN is checked here even if it passed above.
if (this.tag.equals("or") ||
this.tag.equals("my") ||
this.tag.equals("pa-IN") ||
this.tag.equals("gu-IN") ||
this.tag.equals("bn-IN")) {
if (validator.characterIsMissingInFont(this.nativeName.substring(0, 1))) {
return false;
}
}
return true;
}
}
/**
* Not every locale we ship can be used on every device, due to
* font or rendering constraints.
*
* This method filters down the list before generating the descriptor array.
*/
private LocaleDescriptor[] getUsableLocales() {
Collection<String> shippingLocales = BrowserLocaleManager.getPackagedLocaleTags(getContext());
// Future: single-locale builds should be specified, too.
if (shippingLocales == null) {
final String fallbackTag = BrowserLocaleManager.getInstance().getFallbackLocaleTag();
return new LocaleDescriptor[] { new LocaleDescriptor(fallbackTag) };
}
final int initialCount = shippingLocales.size();
final Set<LocaleDescriptor> locales = new HashSet<LocaleDescriptor>(initialCount);
for (String tag : shippingLocales) {
final LocaleDescriptor descriptor = new LocaleDescriptor(tag);
if (!descriptor.isUsable(this.characterValidator)) {
Log.w(LOG_TAG, "Skipping locale " + tag + " on this device.");
continue;
}
locales.add(descriptor);
}
final int usableCount = locales.size();
final LocaleDescriptor[] descriptors = locales.toArray(new LocaleDescriptor[usableCount]);
Arrays.sort(descriptors, 0, usableCount);
return descriptors;
}
@Override
protected void onDialogClosed(boolean positiveResult) {
// The superclass will take care of persistence.
super.onDialogClosed(positiveResult);
// Use this hook to try to fix up the environment ASAP.
// Do this so that the redisplayed fragment is inflated
// with the right locale.
final Locale selectedLocale = getSelectedLocale();
final Context context = getContext();
BrowserLocaleManager.getInstance().updateConfiguration(context, selectedLocale);
}
private Locale getSelectedLocale() {
final String tag = getValue();
if (tag == null || tag.equals("")) {
return Locale.getDefault();
}
return Locales.parseLocaleCode(tag);
}
@Override
public CharSequence getSummary() {
final String value = getValue();
if (TextUtils.isEmpty(value)) {
return getContext().getString(R.string.locale_system_default);
}
// We can't trust super.getSummary() across locale changes,
// apparently, so let's do the same work.
return new LocaleDescriptor(value).getDisplayName();
}
private void buildList() {
final Locale currentLocale = Locale.getDefault();
Log.d(LOG_TAG, "Building locales list. Current locale: " + currentLocale);
if (currentLocale.equals(this.entriesLocale) &&
getEntries() != null) {
Log.v(LOG_TAG, "No need to build list.");
return;
}
final LocaleDescriptor[] descriptors = getUsableLocales();
final int count = descriptors.length;
this.entriesLocale = currentLocale;
// We leave room for "System default".
final String[] entries = new String[count + 1];
final String[] values = new String[count + 1];
entries[0] = getContext().getString(R.string.locale_system_default);
values[0] = "";
for (int i = 0; i < count; ++i) {
final String displayName = descriptors[i].getDisplayName();
final String tag = descriptors[i].getTag();
Log.v(LOG_TAG, displayName + " => " + tag);
entries[i + 1] = displayName;
values[i + 1] = tag;
}
setEntries(entries);
setEntryValues(values);
}
}

View file

@ -0,0 +1,67 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import org.mozilla.gecko.R;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.preference.Preference;
import android.text.Spanned;
import android.text.SpannableStringBuilder;
import android.text.style.ImageSpan;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
class ModifiableHintPreference extends Preference {
private static final String LOGTAG = "ModifiableHintPref";
private final Context mContext;
private final String MATCH_STRING = "%I";
private final int RESID_TEXT_VIEW = R.id.label_search_hint;
private final int RESID_DRAWABLE = R.drawable.ab_add_search_engine;
private final double SCALE_FACTOR = 0.5;
public ModifiableHintPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
public ModifiableHintPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
}
@Override
protected View onCreateView(ViewGroup parent) {
View thisView = super.onCreateView(parent);
configurePreferenceView(thisView);
return thisView;
}
private void configurePreferenceView(View view) {
TextView textView = (TextView) view.findViewById(RESID_TEXT_VIEW);
String searchHint = textView.getText().toString();
// Use an ImageSpan to include the "add search" icon in the Tip.
int imageSpanIndex = searchHint.indexOf(MATCH_STRING);
if (imageSpanIndex != -1) {
// Scale the resource.
Drawable drawable = mContext.getResources().getDrawable(RESID_DRAWABLE);
drawable.setBounds(0, 0, (int) (drawable.getIntrinsicWidth() * SCALE_FACTOR),
(int) (drawable.getIntrinsicHeight() * SCALE_FACTOR));
ImageSpan searchIcon = new ImageSpan(drawable);
final SpannableStringBuilder hintBuilder = new SpannableStringBuilder(searchHint);
// Insert the image.
hintBuilder.setSpan(searchIcon, imageSpanIndex, imageSpanIndex + MATCH_STRING.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(hintBuilder, TextView.BufferType.SPANNABLE);
}
}
}

View file

@ -0,0 +1,271 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import org.mozilla.gecko.R;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.util.PrefUtils;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.content.SharedPreferences;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import java.util.HashSet;
import java.util.Set;
class MultiChoicePreference extends DialogPreference implements DialogInterface.OnMultiChoiceClickListener {
private static final String LOGTAG = "GeckoMultiChoicePreference";
private boolean mValues[];
private boolean mPrevValues[];
private CharSequence mEntryValues[];
private CharSequence mEntries[];
private CharSequence mInitialValues[];
public MultiChoicePreference(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiChoicePreference);
mEntries = a.getTextArray(R.styleable.MultiChoicePreference_entries);
mEntryValues = a.getTextArray(R.styleable.MultiChoicePreference_entryValues);
mInitialValues = a.getTextArray(R.styleable.MultiChoicePreference_initialValues);
a.recycle();
loadPersistedValues();
}
public MultiChoicePreference(Context context) {
this(context, null);
}
/**
* Sets the human-readable entries to be shown in the list. This will be
* shown in subsequent dialogs.
* <p>
* Each entry must have a corresponding index in
* {@link #setEntryValues(CharSequence[])} and
* {@link #setInitialValues(CharSequence[])}.
*
* @param entries The entries.
*/
public void setEntries(CharSequence[] entries) {
mEntries = entries.clone();
}
/**
* @param entriesResId The entries array as a resource.
*/
public void setEntries(int entriesResId) {
setEntries(getContext().getResources().getTextArray(entriesResId));
}
/**
* Sets the preference values for preferences shown in the list.
*
* @param entryValues The entry values.
*/
public void setEntryValues(CharSequence[] entryValues) {
mEntryValues = entryValues.clone();
loadPersistedValues();
}
/**
* Entry values define a separate pref for each row in the dialog.
*
* @param entryValuesResId The entryValues array as a resource.
*/
public void setEntryValues(int entryValuesResId) {
setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
}
/**
* The array of initial entry values in this list. Each entryValue
* corresponds to an entryKey. These values are used if a) the preference
* isn't persisted, or b) the preference is persisted but hasn't yet been
* set.
*
* @param initialValues The entry values
*/
public void setInitialValues(CharSequence[] initialValues) {
mInitialValues = initialValues.clone();
loadPersistedValues();
}
/**
* @param initialValuesResId The initialValues array as a resource.
*/
public void setInitialValues(int initialValuesResId) {
setInitialValues(getContext().getResources().getTextArray(initialValuesResId));
}
/**
* The list of translated strings corresponding to each preference.
*
* @return The array of entries.
*/
public CharSequence[] getEntries() {
return mEntries.clone();
}
/**
* The list of values corresponding to each preference.
*
* @return The array of values.
*/
public CharSequence[] getEntryValues() {
return mEntryValues.clone();
}
/**
* The list of initial values for each preference. Each string in this list
* should be either "true" or "false".
*
* @return The array of initial values.
*/
public CharSequence[] getInitialValues() {
return mInitialValues.clone();
}
public void setValue(final int i, final boolean value) {
mValues[i] = value;
mPrevValues = mValues.clone();
}
/**
* The list of values for each preference. These values are updated after
* the dialog has been displayed.
*
* @return The array of values.
*/
public Set<String> getValues() {
final Set<String> values = new HashSet<String>();
if (mValues == null) {
return values;
}
for (int i = 0; i < mValues.length; i++) {
if (mValues[i]) {
values.add(mEntryValues[i].toString());
}
}
return values;
}
@Override
public void onClick(DialogInterface dialog, int which, boolean val) {
}
@Override
protected void onPrepareDialogBuilder(Builder builder) {
if (mEntries == null || mInitialValues == null || mEntryValues == null) {
throw new IllegalStateException(
"MultiChoicePreference requires entries, entryValues, and initialValues arrays.");
}
if (mEntries.length != mEntryValues.length || mEntries.length != mInitialValues.length) {
throw new IllegalStateException(
"MultiChoicePreference entries, entryValues, and initialValues arrays must be the same length");
}
builder.setMultiChoiceItems(mEntries, mValues, this);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (mPrevValues == null || mInitialValues == null) {
// Initialization is done asynchronously, so these values may not
// have been set before the dialog was closed.
return;
}
if (!positiveResult) {
// user cancelled; reset checkbox values to their previous state
mValues = mPrevValues.clone();
return;
}
mPrevValues = mValues.clone();
if (!callChangeListener(getValues())) {
return;
}
persist();
}
/* Persists the current data stored by this pref to SharedPreferences. */
public boolean persist() {
if (isPersistent()) {
final SharedPreferences.Editor edit = GeckoSharedPrefs.forProfile(getContext()).edit();
final boolean res = persist(edit);
edit.apply();
return res;
}
return false;
}
/* Internal persist method. Take an edit so that multiple prefs can be persisted in a single commit. */
protected boolean persist(SharedPreferences.Editor edit) {
if (isPersistent()) {
Set<String> vals = getValues();
PrefUtils.putStringSet(edit, getKey(), vals).apply();;
return true;
}
return false;
}
/* Returns a list of EntryValues that are currently enabled. */
public Set<String> getPersistedStrings(Set<String> defaultVal) {
if (!isPersistent()) {
return defaultVal;
}
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext());
return PrefUtils.getStringSet(prefs, getKey(), defaultVal);
}
/**
* Loads persistent prefs from shared preferences. If the preferences
* aren't persistent or haven't yet been stored, they will be set to their
* initial values.
*/
protected void loadPersistedValues() {
final int entryCount = mInitialValues.length;
mValues = new boolean[entryCount];
if (entryCount != mEntries.length || entryCount != mEntryValues.length) {
throw new IllegalStateException(
"MultiChoicePreference entryValues and initialValues arrays must be the same length");
}
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final Set<String> stringVals = getPersistedStrings(null);
for (int i = 0; i < entryCount; i++) {
if (stringVals != null) {
mValues[i] = stringVals.contains(mEntryValues[i]);
} else {
final boolean defaultVal = mInitialValues[i].equals("true");
mValues[i] = defaultVal;
}
}
mPrevValues = mValues.clone();
}
});
}
}

View file

@ -0,0 +1,116 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import org.mozilla.gecko.R;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.content.SharedPreferences;
import android.widget.Button;
import android.util.AttributeSet;
import android.util.Log;
import java.util.Set;
/* Provides backwards compatibility for some old multi-choice pref types used by Gecko.
* This will import the old data from the old prefs the first time it is run.
*/
class MultiPrefMultiChoicePreference extends MultiChoicePreference {
private static final String LOGTAG = "GeckoMultiPrefPreference";
private static final String IMPORT_SUFFIX = "_imported_";
private final CharSequence[] keys;
public MultiPrefMultiChoicePreference(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiPrefMultiChoicePreference);
keys = a.getTextArray(R.styleable.MultiPrefMultiChoicePreference_entryKeys);
a.recycle();
loadPersistedValues();
}
// Helper method for reading a boolean pref.
private boolean getPersistedBoolean(SharedPreferences prefs, String key, boolean defaultReturnValue) {
if (!isPersistent()) {
return defaultReturnValue;
}
return prefs.getBoolean(key, defaultReturnValue);
}
// Overridden to do a one time import for the old preference type to the new one.
@Override
protected synchronized void loadPersistedValues() {
// This will load the new pref if it exists.
super.loadPersistedValues();
// First check if we've already done the import the old data. If so, nothing to load.
final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext());
final boolean imported = getPersistedBoolean(prefs, getKey() + IMPORT_SUFFIX, false);
if (imported) {
return;
}
// Load the data we'll need to find the old style prefs
final CharSequence[] init = getInitialValues();
final CharSequence[] entries = getEntries();
if (keys == null || init == null) {
return;
}
final int entryCount = keys.length;
if (entryCount != entries.length || entryCount != init.length) {
throw new IllegalStateException("MultiChoicePreference entryKeys and initialValues arrays must be the same length");
}
// Now iterate through the entries on a background thread.
final SharedPreferences.Editor edit = prefs.edit();
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
try {
// Use one editor to batch as many changes as we can.
for (int i = 0; i < entryCount; i++) {
String key = keys[i].toString();
boolean initialValue = "true".equals(init[i]);
boolean val = getPersistedBoolean(prefs, key, initialValue);
// Save the pref and remove the old preference.
setValue(i, val);
edit.remove(key);
}
persist(edit);
edit.putBoolean(getKey() + IMPORT_SUFFIX, true);
edit.apply();
} catch (Exception ex) {
Log.i(LOGTAG, "Err", ex);
}
}
});
}
@Override
public void onClick(DialogInterface dialog, int which, boolean val) {
// enable positive button only if at least one item is checked
boolean enabled = false;
final Set<String> values = getValues();
enabled = (values.size() > 0);
final Button button = ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE);
if (button.isEnabled() != enabled) {
button.setEnabled(enabled);
}
}
}

View file

@ -0,0 +1,255 @@
/* 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 org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.Property;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnShowListener;
import android.content.res.Resources;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class PanelsPreference extends CustomListPreference {
protected String LOGTAG = "PanelsPreference";
// Position state of this Preference in enclosing category.
private static final int STATE_IS_FIRST = 0;
private static final int STATE_IS_LAST = 1;
/**
* Index of the context menu button for controlling display options.
* For (removable) Dynamic panels, this button removes the panel.
* For built-in panels, this button toggles showing or hiding the panel.
*/
private static final int INDEX_DISPLAY_BUTTON = 1;
private static final int INDEX_REORDER_BUTTON = 2;
// Indices of buttons in context menu for reordering.
private static final int INDEX_MOVE_UP_BUTTON = 0;
private static final int INDEX_MOVE_DOWN_BUTTON = 1;
private String LABEL_HIDE;
private String LABEL_SHOW;
private View preferenceView;
protected boolean mIsHidden;
private final boolean mIsRemovable;
private boolean mAnimate;
private static final int ANIMATION_DURATION_MS = 400;
// State for reordering.
private int mPositionState = -1;
private final int mIndex;
public PanelsPreference(Context context, CustomListCategory parentCategory, boolean isRemovable, int index, boolean animate) {
super(context, parentCategory);
mIsRemovable = isRemovable;
mIndex = index;
mAnimate = animate;
}
@Override
protected int getPreferenceLayoutResource() {
return R.layout.preference_panels;
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
// Override view handling so we can grey out "hidden" PanelPreferences.
view.setEnabled(!mIsHidden);
if (view instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
group.getChildAt(i).setEnabled(!mIsHidden);
}
preferenceView = group;
}
if (mAnimate) {
ViewHelper.setAlpha(preferenceView, 0);
final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION_MS);
animator.attach(preferenceView, Property.ALPHA, 1);
animator.start();
// Clear animate flag.
mAnimate = false;
}
}
@Override
protected String[] createDialogItems() {
final Resources res = getContext().getResources();
final String labelReorder = res.getString(R.string.pref_panels_reorder);
if (mIsRemovable) {
return new String[] { LABEL_SET_AS_DEFAULT, LABEL_REMOVE, labelReorder };
}
// Built-in panels can't be removed, so use show/hide options.
LABEL_HIDE = res.getString(R.string.pref_panels_hide);
LABEL_SHOW = res.getString(R.string.pref_panels_show);
return new String[] { LABEL_SET_AS_DEFAULT, LABEL_HIDE, labelReorder };
}
@Override
public void setIsDefault(boolean isDefault) {
mIsDefault = isDefault;
if (isDefault) {
setSummary(LABEL_IS_DEFAULT);
if (mIsHidden) {
// Unhide the panel if it's being set as the default.
setHidden(false);
}
} else {
setSummary("");
}
}
@Override
protected void onDialogIndexClicked(int index) {
switch (index) {
case INDEX_SET_DEFAULT_BUTTON:
mParentCategory.setDefault(this);
break;
case INDEX_DISPLAY_BUTTON:
// Handle display options for the panel.
if (mIsRemovable) {
// For removable panels, the button displays text for removing the panel.
mParentCategory.uninstall(this);
} else {
// Otherwise, the button toggles between text for showing or hiding the panel.
((PanelsPreferenceCategory) mParentCategory).setHidden(this, !mIsHidden);
}
break;
case INDEX_REORDER_BUTTON:
// Display dialog for changing preference order.
final Dialog orderDialog = makeReorderDialog();
orderDialog.show();
break;
default:
Log.w(LOGTAG, "Selected index out of range: " + index);
}
}
@Override
protected void configureShownDialog() {
super.configureShownDialog();
// Handle Show/Hide buttons.
if (!mIsRemovable) {
final TextView hideButton = (TextView) mDialog.getListView().getChildAt(INDEX_DISPLAY_BUTTON);
hideButton.setText(mIsHidden ? LABEL_SHOW : LABEL_HIDE);
}
}
private Dialog makeReorderDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
final Resources res = getContext().getResources();
final String labelUp = res.getString(R.string.pref_panels_move_up);
final String labelDown = res.getString(R.string.pref_panels_move_down);
builder.setTitle(getTitle());
builder.setItems(new String[] { labelUp, labelDown }, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int index) {
dialog.dismiss();
switch (index) {
case INDEX_MOVE_UP_BUTTON:
((PanelsPreferenceCategory) mParentCategory).moveUp(PanelsPreference.this);
break;
case INDEX_MOVE_DOWN_BUTTON:
((PanelsPreferenceCategory) mParentCategory).moveDown(PanelsPreference.this);
break;
}
}
});
final Dialog dialog = builder.create();
dialog.setOnShowListener(new OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
setReorderItemsEnabled(dialog);
}
});
return dialog;
}
public void setIsFirst() {
mPositionState = STATE_IS_FIRST;
}
public void setIsLast() {
mPositionState = STATE_IS_LAST;
}
/**
* Configure enabled state of the reorder dialog, which must be done after the dialog is shown.
* @param dialog Dialog to configure
*/
private void setReorderItemsEnabled(DialogInterface dialog) {
// Update button enabled-ness for reordering.
switch (mPositionState) {
case STATE_IS_FIRST:
final TextView itemUp = (TextView) ((AlertDialog) dialog).getListView().getChildAt(INDEX_MOVE_UP_BUTTON);
itemUp.setEnabled(false);
// Disable clicks to this view.
itemUp.setOnClickListener(null);
break;
case STATE_IS_LAST:
final TextView itemDown = (TextView) ((AlertDialog) dialog).getListView().getChildAt(INDEX_MOVE_DOWN_BUTTON);
itemDown.setEnabled(false);
// Disable clicks to this view.
itemDown.setOnClickListener(null);
break;
default:
// Do nothing.
break;
}
}
public void setHidden(boolean toHide) {
if (toHide) {
setIsDefault(false);
}
if (mIsHidden != toHide) {
mIsHidden = toHide;
notifyChanged();
}
}
public boolean isHidden() {
return mIsHidden;
}
public int getIndex() {
return mIndex;
}
}

View file

@ -0,0 +1,261 @@
/* 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 org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.TelemetryContract.Method;
import org.mozilla.gecko.home.HomeConfig;
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
import org.mozilla.gecko.home.HomeConfig.State;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
public class PanelsPreferenceCategory extends CustomListCategory {
public static final String LOGTAG = "PanelsPrefCategory";
protected HomeConfig mHomeConfig;
protected HomeConfig.Editor mConfigEditor;
protected UIAsyncTask.WithoutParams<State> mLoadTask;
public PanelsPreferenceCategory(Context context) {
super(context);
initConfig(context);
}
public PanelsPreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
initConfig(context);
}
public PanelsPreferenceCategory(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initConfig(context);
}
protected void initConfig(Context context) {
mHomeConfig = HomeConfig.getDefault(context);
}
@Override
public void onAttachedToActivity() {
super.onAttachedToActivity();
loadHomeConfig(null);
}
/**
* Load the Home Panels config and populate the preferences screen and maintain local state.
*/
private void loadHomeConfig(final String animatePanelId) {
mLoadTask = new UIAsyncTask.WithoutParams<State>(ThreadUtils.getBackgroundHandler()) {
@Override
public HomeConfig.State doInBackground() {
return mHomeConfig.load();
}
@Override
public void onPostExecute(HomeConfig.State configState) {
mConfigEditor = configState.edit();
displayHomeConfig(configState, animatePanelId);
}
};
mLoadTask.execute();
}
/**
* Simplified refresh of Home Panels when there is no state to be persisted.
*/
public void refresh() {
refresh(null, null);
}
/**
* Refresh the Home Panels list and animate a panel, if specified.
* If null, load from HomeConfig.
*
* @param State HomeConfig.State to rebuild Home Panels list from.
* @param String panelId of panel to be animated.
*/
public void refresh(State state, String animatePanelId) {
// Clear all the existing home panels.
removeAll();
if (state == null) {
loadHomeConfig(animatePanelId);
} else {
displayHomeConfig(state, animatePanelId);
}
}
private void displayHomeConfig(HomeConfig.State configState, String animatePanelId) {
int index = 0;
for (PanelConfig panelConfig : configState) {
final boolean isRemovable = panelConfig.isDynamic();
// Create and add the pref.
final String panelId = panelConfig.getId();
final boolean animate = TextUtils.equals(animatePanelId, panelId);
final PanelsPreference pref = new PanelsPreference(getContext(), PanelsPreferenceCategory.this, isRemovable, index, animate);
pref.setTitle(panelConfig.getTitle());
pref.setKey(panelConfig.getId());
// XXX: Pull icon from PanelInfo.
addPreference(pref);
if (panelConfig.isDisabled()) {
pref.setHidden(true);
}
index++;
}
setPositionState();
setDefaultFromConfig();
}
private void setPositionState() {
final int prefCount = getPreferenceCount();
// Pass in position state to first and last preference.
final PanelsPreference firstPref = (PanelsPreference) getPreference(0);
firstPref.setIsFirst();
final PanelsPreference lastPref = (PanelsPreference) getPreference(prefCount - 1);
lastPref.setIsLast();
}
private void setDefaultFromConfig() {
final String defaultPanelId = mConfigEditor.getDefaultPanelId();
if (defaultPanelId == null) {
mDefaultReference = null;
return;
}
final int prefCount = getPreferenceCount();
for (int i = 0; i < prefCount; i++) {
final PanelsPreference pref = (PanelsPreference) getPreference(i);
if (defaultPanelId.equals(pref.getKey())) {
super.setDefault(pref);
break;
}
}
}
@Override
public void setDefault(CustomListPreference pref) {
super.setDefault(pref);
final String id = pref.getKey();
final String defaultPanelId = mConfigEditor.getDefaultPanelId();
if (defaultPanelId != null && defaultPanelId.equals(id)) {
return;
}
updateVisibilityPrefsForPanel(id, true);
mConfigEditor.setDefault(id);
mConfigEditor.apply();
Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_SET_DEFAULT, Method.DIALOG, id);
}
@Override
protected void onPrepareForRemoval() {
super.onPrepareForRemoval();
if (mLoadTask != null) {
mLoadTask.cancel();
}
}
@Override
public void uninstall(CustomListPreference pref) {
final String id = pref.getKey();
mConfigEditor.uninstall(id);
mConfigEditor.apply();
Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_REMOVE, Method.DIALOG, id);
super.uninstall(pref);
}
public void moveUp(PanelsPreference pref) {
final int panelIndex = pref.getIndex();
if (panelIndex > 0) {
final String panelKey = pref.getKey();
mConfigEditor.moveTo(panelKey, panelIndex - 1);
final State state = mConfigEditor.apply();
Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_MOVE, Method.DIALOG, panelKey);
refresh(state, panelKey);
}
}
public void moveDown(PanelsPreference pref) {
final int panelIndex = pref.getIndex();
if (panelIndex < getPreferenceCount() - 1) {
final String panelKey = pref.getKey();
mConfigEditor.moveTo(panelKey, panelIndex + 1);
final State state = mConfigEditor.apply();
Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_MOVE, Method.DIALOG, panelKey);
refresh(state, panelKey);
}
}
/**
* Update the hide/show state of the preference and save the HomeConfig
* changes.
*
* @param pref Preference to update
* @param toHide New hidden state of the preference
*/
protected void setHidden(PanelsPreference pref, boolean toHide) {
final String id = pref.getKey();
mConfigEditor.setDisabled(id, toHide);
mConfigEditor.apply();
if (toHide) {
Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_HIDE, Method.DIALOG, id);
} else {
Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_SHOW, Method.DIALOG, id);
}
updateVisibilityPrefsForPanel(id, !toHide);
pref.setHidden(toHide);
setDefaultFromConfig();
}
/**
* When the default panel is removed or disabled, find an enabled panel
* if possible and set it as mDefaultReference.
*/
@Override
protected void setFallbackDefault() {
setDefaultFromConfig();
}
private void updateVisibilityPrefsForPanel(String panelId, boolean toShow) {
if (HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS).equals(panelId)) {
GeckoSharedPrefs.forProfile(getContext()).edit().putBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, toShow).apply();
}
if (HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.COMBINED_HISTORY).equals(panelId)) {
GeckoSharedPrefs.forProfile(getContext()).edit().putBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, toShow).apply();
}
}
}

View file

@ -0,0 +1,67 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.icons.storage.DiskStorage;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
class PrivateDataPreference extends MultiPrefMultiChoicePreference {
private static final String LOGTAG = "GeckoPrivateDataPreference";
private static final String PREF_KEY_PREFIX = "private.data.";
public PrivateDataPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (!positiveResult) {
return;
}
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.DIALOG, "settings");
final Set<String> values = getValues();
final JSONObject json = new JSONObject();
for (String value : values) {
// Privacy pref checkbox values are stored in Android prefs to
// remember their check states. The key names are private.data.X,
// where X is a string from Gecko sanitization. This prefix is
// removed here so we can send the values to Gecko, which then does
// the sanitization for each key.
final String key = value.substring(PREF_KEY_PREFIX.length());
try {
json.put(key, true);
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error", e);
}
}
if (values.contains("private.data.offlineApps")) {
// Remove all icons from storage if removing "Offline website data" was selected.
DiskStorage.get(getContext()).evictAll();
}
// clear private data in gecko
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
}
}

View file

@ -0,0 +1,183 @@
/* 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 org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SnackbarBuilder;
import org.mozilla.gecko.icons.IconCallback;
import org.mozilla.gecko.icons.IconDescriptor;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.Icons;
import org.mozilla.gecko.widget.FaviconView;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.support.design.widget.Snackbar;
import android.text.SpannableString;
import android.util.Log;
import android.view.View;
/**
* Represents an element in the list of search engines on the preferences menu.
*/
public class SearchEnginePreference extends CustomListPreference {
protected String LOGTAG = "SearchEnginePreference";
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 final Object bitmapLock = new Object();
private FaviconView mFaviconView;
// Search engine identifier specified by the gecko search service. This will be "other"
// for engines that are not shipped with the app.
private String mIdentifier;
public SearchEnginePreference(Context context, SearchPreferenceCategory parentCategory) {
super(context, parentCategory);
}
/**
* Called by Android when we're bound to the custom view. Allows us to set the custom properties
* of our custom view elements as we desire (We can now use findViewById on them).
*
* @param view The view instance for this Preference object.
*/
@Override
protected void onBindView(View view) {
super.onBindView(view);
// We synchronise to avoid a race condition between this and the favicon loading callback in
// setSearchEngineFromJSON.
synchronized (bitmapLock) {
// Set the icon in the FaviconView.
mFaviconView = ((FaviconView) view.findViewById(R.id.search_engine_icon));
if (mIconBitmap != null) {
mFaviconView.updateAndScaleImage(IconResponse.create(mIconBitmap));
}
}
}
@Override
protected int getPreferenceLayoutResource() {
return R.layout.preference_search_engine;
}
/**
* Returns the strings to be displayed in the dialog.
*/
@Override
protected String[] createDialogItems() {
return new String[] { LABEL_SET_AS_DEFAULT,
LABEL_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) {
Activity activity = (Activity) getContext();
SnackbarBuilder.builder(activity)
.message(R.string.pref_search_last_toast)
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
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;
}
}
/**
* @return Identifier of built-in search engine, or "other" if engine is not built-in.
*/
public String getIdentifier() {
return mIdentifier;
}
/**
* Configure this Preference object from the Gecko search engine JSON object.
* @param geckoEngineJSON The Gecko-formatted JSON object representing the search engine.
* @throws JSONException If the JSONObject is invalid.
*/
public void setSearchEngineFromJSON(JSONObject geckoEngineJSON) throws JSONException {
mIdentifier = geckoEngineJSON.getString("identifier");
// A null JS value gets converted into a string.
if (mIdentifier.equals("null")) {
mIdentifier = "other";
}
final String engineName = geckoEngineJSON.getString("name");
final SpannableString titleSpannable = new SpannableString(engineName);
setTitle(titleSpannable);
final String iconURI = geckoEngineJSON.getString("iconURI");
// Keep a reference to the bitmap - we'll need it later in onBindView.
try {
Icons.with(getContext())
.pageUrl(mIdentifier)
.icon(IconDescriptor.createGenericIcon(iconURI))
.privileged(true)
.build()
.execute(new IconCallback() {
@Override
public void onIconResponse(IconResponse response) {
mIconBitmap = response.getBitmap();
if (mFaviconView != null) {
mFaviconView.updateAndScaleImage(response);
}
}
});
} catch (IllegalArgumentException e) {
Log.e(LOGTAG, "IllegalArgumentException creating Bitmap. Most likely a zero-length bitmap.", e);
} catch (NullPointerException e) {
Log.e(LOGTAG, "NullPointerException creating Bitmap. Most likely a zero-length bitmap.", e);
}
}
}

View file

@ -0,0 +1,145 @@
/* 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.Preference;
import android.util.AttributeSet;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.TelemetryContract.Method;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
public class SearchPreferenceCategory extends CustomListCategory implements GeckoEventListener {
public static final String LOGTAG = "SearchPrefCategory";
public SearchPreferenceCategory(Context context) {
super(context);
}
public SearchPreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SearchPreferenceCategory(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onAttachedToActivity() {
super.onAttachedToActivity();
// Register for SearchEngines messages and request list of search engines from Gecko.
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "SearchEngines:Data");
GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
}
@Override
protected void onPrepareForRemoval() {
super.onPrepareForRemoval();
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "SearchEngines:Data");
}
@Override
public void setDefault(CustomListPreference item) {
super.setDefault(item);
sendGeckoEngineEvent("SearchEngines:SetDefault", item.getTitle().toString());
final String identifier = ((SearchEnginePreference) item).getIdentifier();
Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_SET_DEFAULT, Method.DIALOG, identifier);
}
@Override
public void uninstall(CustomListPreference item) {
super.uninstall(item);
sendGeckoEngineEvent("SearchEngines:Remove", item.getTitle().toString());
final String identifier = ((SearchEnginePreference) item).getIdentifier();
Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_REMOVE, Method.DIALOG, identifier);
}
@Override
public void handleMessage(String event, final JSONObject data) {
if (event.equals("SearchEngines:Data")) {
// Parse engines array from JSON.
JSONArray engines;
try {
engines = data.getJSONArray("searchEngines");
} catch (JSONException e) {
Log.e(LOGTAG, "Unable to decode search engine data from Gecko.", e);
return;
}
// Clear the preferences category from this thread.
this.removeAll();
// Create an element in this PreferenceCategory for each engine.
for (int i = 0; i < engines.length(); i++) {
try {
final JSONObject engineJSON = engines.getJSONObject(i);
final SearchEnginePreference enginePreference = new SearchEnginePreference(getContext(), this);
enginePreference.setSearchEngineFromJSON(engineJSON);
enginePreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
SearchEnginePreference sPref = (SearchEnginePreference) preference;
// Display the configuration dialog associated with the tapped engine.
sPref.showDialog();
return true;
}
});
addPreference(enginePreference);
// The first element in the array is the default engine.
if (i == 0) {
// 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.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
enginePreference.setIsDefault(true);
}
});
mDefaultReference = enginePreference;
}
} catch (JSONException e) {
Log.e(LOGTAG, "JSONException parsing engine at index " + i, e);
}
}
}
}
/**
* 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, String engineName) {
JSONObject json = new JSONObject();
try {
json.put("engine", engineName);
} catch (JSONException e) {
Log.e(LOGTAG, "JSONException creating search engine configuration change message for Gecko.", e);
return;
}
GeckoAppShell.notifyObservers(event, json.toString());
}
}

View file

@ -0,0 +1,124 @@
/* -*- 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.preferences;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
import android.app.AlertDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.DialogPreference;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
public class SetHomepagePreference extends DialogPreference {
private static final String DEFAULT_HOMEPAGE = AboutPages.HOME;
private final SharedPreferences prefs;
private RadioGroup homepageLayout;
private RadioButton defaultRadio;
private RadioButton userAddressRadio;
private EditText homepageEditText;
// This is the url that 1) was loaded from prefs or, 2) stored
// when the user pressed the "default homepage" checkbox.
private String storedUrl;
public SetHomepagePreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
prefs = GeckoSharedPrefs.forProfile(context);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
// Without this GB devices have a black background to the dialog.
builder.setInverseBackgroundForced(true);
}
@Override
protected void onBindDialogView(final View view) {
super.onBindDialogView(view);
homepageLayout = (RadioGroup) view.findViewById(R.id.homepage_layout);
defaultRadio = (RadioButton) view.findViewById(R.id.radio_default);
userAddressRadio = (RadioButton) view.findViewById(R.id.radio_user_address);
homepageEditText = (EditText) view.findViewById(R.id.edittext_user_address);
storedUrl = prefs.getString(GeckoPreferences.PREFS_HOMEPAGE, DEFAULT_HOMEPAGE);
homepageLayout.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(final RadioGroup radioGroup, final int checkedId) {
if (checkedId == R.id.radio_user_address) {
homepageEditText.setVisibility(View.VISIBLE);
openKeyboardAndSelectAll(getContext(), homepageEditText);
} else {
homepageEditText.setVisibility(View.GONE);
}
}
});
setUIState(storedUrl);
}
private void setUIState(final String url) {
if (isUrlDefaultHomepage(url)) {
defaultRadio.setChecked(true);
} else {
userAddressRadio.setChecked(true);
homepageEditText.setText(url);
}
}
private boolean isUrlDefaultHomepage(final String url) {
return TextUtils.isEmpty(url) || DEFAULT_HOMEPAGE.equals(url);
}
private static void openKeyboardAndSelectAll(final Context context, final View viewToFocus) {
viewToFocus.requestFocus();
viewToFocus.post(new Runnable() {
@Override
public void run() {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(viewToFocus, InputMethodManager.SHOW_IMPLICIT);
// android:selectAllOnFocus doesn't work for the initial focus:
// I'm not sure why. We manually selectAll instead.
if (viewToFocus instanceof EditText) {
((EditText) viewToFocus).selectAll();
}
}
});
}
@Override
protected void onDialogClosed(final boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult) {
final SharedPreferences.Editor editor = prefs.edit();
final String homePageEditTextValue = homepageEditText.getText().toString();
final String newPrefValue;
if (homepageLayout.getCheckedRadioButtonId() == R.id.radio_default ||
isUrlDefaultHomepage(homePageEditTextValue)) {
newPrefValue = "";
editor.remove(GeckoPreferences.PREFS_HOMEPAGE);
} else {
newPrefValue = homePageEditTextValue;
editor.putString(GeckoPreferences.PREFS_HOMEPAGE, newPrefValue);
}
editor.apply();
if (getOnPreferenceChangeListener() != null) {
getOnPreferenceChangeListener().onPreferenceChange(this, newPrefValue);
}
}
}
}

View file

@ -0,0 +1,103 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences;
import android.content.Context;
import android.content.Intent;
import android.preference.Preference;
import android.text.TextUtils;
import android.util.AttributeSet;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.TelemetryContract.Method;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
import org.mozilla.gecko.fxa.activities.PicassoPreferenceIconTarget;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.util.ThreadUtils;
class SyncPreference extends Preference {
private final Context mContext;
private final Target profileAvatarTarget;
public SyncPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
final float cornerRadius = mContext.getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2;
profileAvatarTarget = new PicassoPreferenceIconTarget(mContext.getResources(), this, cornerRadius);
}
private void launchFxASetup() {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
mContext.startActivity(intent);
}
public void update(final AndroidFxAccount fxAccount) {
if (fxAccount == null) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
setTitle(R.string.pref_sync);
setSummary(R.string.pref_sync_summary);
// Cancel any pending task.
Picasso.with(mContext).cancelRequest(profileAvatarTarget);
// Clear previously set icon.
// Bug 1312719 - IconDrawable is prior to IconResId, drawable must be set null before setIcon(resId)
// http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/java/android/preference/Preference.java#562
setIcon(null);
setIcon(R.drawable.sync_avatar_default);
}
});
return;
}
// Update title from account email.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
setTitle(fxAccount.getEmail());
setSummary("");
}
});
final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON();
if (profileJSON == null) {
return;
}
// Avatar URI empty, return early.
final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR);
if (TextUtils.isEmpty(avatarURI)) {
return;
}
Picasso.with(mContext)
.load(avatarURI)
.centerInside()
.resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height)
.placeholder(R.drawable.sync_avatar_default)
.error(R.drawable.sync_avatar_default)
.into(profileAvatarTarget);
}
@Override
protected void onClick() {
// Launch the FxA "Get started" activity, which will dispatch to the
// right location.
launchFxASetup();
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.SETTINGS, "sync_setup");
}
}