mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 15:58:39 +09:00
pt 1 in reviving the android build (copied from pm 28a1, wish me luck)
This commit is contained in:
parent
efa9662725
commit
d7788a6d6d
4249 changed files with 468189 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue