Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2014-03-04 14:55:13 +01:00
commit c98ef60cc1
51 changed files with 1957 additions and 561 deletions

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>

View File

@ -4,6 +4,6 @@
"branch": "",
"revision": ""
},
"revision": "ac8b988b122a95d657c730dcdfabbebf03385b43",
"revision": "020bf0df084e3e80e51f9dfd5fdeef2e8ab90452",
"repo_path": "/integration/gaia-central"
}

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -15,7 +15,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="6df4533f14ec2645bb13d8a803a5151583ca13a8"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="71f78f7f68fcf5e23cc5965fee0dad45289c438b"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -51,6 +51,7 @@ this.UITour = {
pinnedTabs: new WeakMap(),
urlbarCapture: new WeakMap(),
appMenuOpenForAnnotation: new Set(),
availableTargetsCache: new WeakMap(),
_detachingTab: false,
_queuedEvents: [],
@ -105,7 +106,7 @@ this.UITour = {
let element = aDocument.getAnonymousElementByAttribute(selectedtab,
"anonid",
"tab-icon-image");
if (!element || !this.isElementVisible(element)) {
if (!element || !UITour.isElementVisible(element)) {
return null;
}
return element;
@ -128,6 +129,19 @@ this.UITour = {
UITelemetry.addSimpleMeasureFunction("UITour",
this.getTelemetry.bind(this));
// Clear the availableTargetsCache on widget changes.
let listenerMethods = [
"onWidgetAdded",
"onWidgetMoved",
"onWidgetRemoved",
"onWidgetReset",
"onAreaReset",
];
CustomizableUI.addListener(listenerMethods.reduce((listener, method) => {
listener[method] = () => this.availableTargetsCache.clear();
return listener;
}, {}));
},
restoreSeenPageIDs: function() {
@ -920,8 +934,8 @@ this.UITour = {
},
showMenu: function(aWindow, aMenuName, aOpenCallback = null) {
function openMenuButton(aId) {
let menuBtn = aWindow.document.getElementById(aId);
function openMenuButton(aID) {
let menuBtn = aWindow.document.getElementById(aID);
if (!menuBtn || !menuBtn.boxObject) {
aOpenCallback();
return;
@ -953,8 +967,8 @@ this.UITour = {
},
hideMenu: function(aWindow, aMenuName) {
function closeMenuButton(aId) {
let menuBtn = aWindow.document.getElementById(aId);
function closeMenuButton(aID) {
let menuBtn = aWindow.document.getElementById(aID);
if (menuBtn && menuBtn.boxObject)
menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false);
}
@ -1042,19 +1056,53 @@ this.UITour = {
aWindow.gBrowser.selectedTab = tab;
},
getConfiguration: function(aContentDocument, aConfiguration, aCallbackId) {
let config = null;
getConfiguration: function(aContentDocument, aConfiguration, aCallbackID) {
switch (aConfiguration) {
case "availableTargets":
this.getAvailableTargets(aContentDocument, aCallbackID);
break;
case "sync":
config = {
this.sendPageCallback(aContentDocument, aCallbackID, {
setup: Services.prefs.prefHasUserValue("services.sync.username"),
};
});
break;
default:
Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration);
break;
}
this.sendPageCallback(aContentDocument, aCallbackId, config);
},
getAvailableTargets: function(aContentDocument, aCallbackID) {
let window = this.getChromeWindow(aContentDocument);
let data = this.availableTargetsCache.get(window);
if (data) {
this.sendPageCallback(aContentDocument, aCallbackID, data);
return;
}
let promises = [];
for (let targetName of this.targets.keys()) {
promises.push(this.getTarget(window, targetName));
}
Promise.all(promises).then((targetObjects) => {
let targetNames = [
"pinnedTab",
];
for (let targetObject of targetObjects) {
if (targetObject.node)
targetNames.push(targetObject.targetName);
}
let data = {
targets: targetNames,
};
this.availableTargetsCache.set(window, data);
this.sendPageCallback(aContentDocument, aCallbackID, data);
}, (err) => {
Cu.reportError(err);
this.sendPageCallback(aContentDocument, aCallbackID, {
targets: [],
});
});
},
};

View File

@ -11,6 +11,7 @@ support-files =
skip-if = os == "linux" # Intermittent failures, bug 951965
[browser_UITour2.js]
[browser_UITour3.js]
[browser_UITour_availableTargets.js]
[browser_UITour_panel_close_annotation.js]
[browser_UITour_detach_tab.js]
[browser_UITour_registerPageID.js]

View File

@ -0,0 +1,83 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}
let tests = [
function test_availableTargets(done) {
gContentAPI.getConfiguration("availableTargets", (data) => {
ok_targets(data, [
"accountStatus",
"addons",
"appMenu",
"backForward",
"bookmarks",
"customize",
"help",
"home",
"pinnedTab",
"quit",
"search",
"searchProvider",
"urlbar",
]);
ok(UITour.availableTargetsCache.has(window),
"Targets should now be cached");
done();
});
},
function test_availableTargets_changeWidgets(done) {
CustomizableUI.removeWidgetFromArea("bookmarks-menu-button");
ok(!UITour.availableTargetsCache.has(window),
"Targets should be evicted from cache after widget change");
gContentAPI.getConfiguration("availableTargets", (data) => {
ok_targets(data, [
"accountStatus",
"addons",
"appMenu",
"backForward",
"customize",
"help",
"home",
"pinnedTab",
"quit",
"search",
"searchProvider",
"urlbar",
]);
ok(UITour.availableTargetsCache.has(window),
"Targets should now be cached again");
CustomizableUI.reset();
ok(!UITour.availableTargetsCache.has(window),
"Targets should not be cached after reset");
done();
});
},
];
function ok_targets(actualData, expectedTargets) {
// Depending on how soon after page load this is called, the selected tab icon
// may or may not be showing the loading throbber. Check for its presence and
// insert it into expectedTargets if it's visible.
let selectedTabIcon =
document.getAnonymousElementByAttribute(gBrowser.selectedTab,
"anonid",
"tab-icon-image");
if (selectedTabIcon && UITour.isElementVisible(selectedTabIcon))
expectedTargets.push("selectedTabIcon");
ok(Array.isArray(actualData.targets), "data.targets should be an array");
is(actualData.targets.sort().toString(), expectedTargets.sort().toString(),
"Targets should be as expected");
}

View File

@ -23,7 +23,7 @@ let tests = [
done();
}
gContentAPI.getSyncConfiguration(callback);
gContentAPI.getConfiguration("sync", callback);
},
function test_checkSyncSetup_enabled(done) {
@ -33,6 +33,6 @@ let tests = [
}
Services.prefs.setCharPref("services.sync.username", "uitour@tests.mozilla.org");
gContentAPI.getSyncConfiguration(callback);
gContentAPI.getConfiguration("sync", callback);
},
];

View File

@ -160,10 +160,10 @@ if (typeof Mozilla == 'undefined') {
});
};
Mozilla.UITour.getSyncConfiguration = function(callback) {
Mozilla.UITour.getConfiguration = function(configName, callback) {
_sendEvent('getConfiguration', {
callbackID: _waitForCallback(callback),
configuration: "sync",
configuration: configName,
});
};
})();

View File

@ -167,7 +167,7 @@ toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-p
#bookmarked-notification-anchor[notification="finish"] > #bookmarked-notification {
background-image: url("chrome://browser/skin/places/bookmarks-notification-finish.png");
animation: animation-bookmarkAdded 800ms;
animation-timing-function: ease ease ease linear;
animation-timing-function: ease, ease, ease;
}
#bookmarked-notification-anchor[notification="finish"][in-bookmarks-toolbar=true] > #bookmarked-notification {

View File

@ -404,7 +404,7 @@ toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-p
#bookmarked-notification-anchor[notification="finish"] > #bookmarked-notification {
background-image: url("chrome://browser/skin/places/bookmarks-notification-finish.png");
animation: animation-bookmarkAdded 800ms;
animation-timing-function: ease ease ease linear;
animation-timing-function: ease, ease, ease;
}
#bookmarked-notification-anchor[notification="finish"][in-bookmarks-toolbar=true] > #bookmarked-notification {

View File

@ -354,7 +354,7 @@ toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-p
#bookmarked-notification-anchor[notification="finish"] > #bookmarked-notification {
background-image: url("chrome://browser/skin/places/bookmarks-notification-finish.png");
animation: animation-bookmarkAdded 800ms;
animation-timing-function: ease ease ease linear;
animation-timing-function: ease, ease, ease;
}
#bookmarks-menu-button[notification="finish"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {

View File

@ -511,6 +511,16 @@ char* Pickle::BeginWrite(uint32_t length, uint32_t alignment) {
DCHECK(intptr_t(buffer) % alignment == 0);
header_->payload_size = new_size;
#ifdef MOZ_VALGRIND
// pad the trailing end as well, so that valgrind
// doesn't complain when we write the buffer
padding = AlignInt(length) - length;
if (padding) {
memset(buffer + length, kBytePaddingMarker, padding);
}
#endif
return buffer;
}

View File

@ -14,6 +14,7 @@ import java.util.Vector;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.db.BrowserContract.Combined;
@ -2107,7 +2108,7 @@ abstract public class BrowserApp extends GeckoApp
MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
MenuItem enterGuestMode = aMenu.findItem(R.id.new_guest_session);
MenuItem exitGuestMode = aMenu.findItem(R.id.exit_guest_session);
MenuItem subscribe = aMenu.findItem(R.id.subscribe);
MenuItem subscribe = aMenu.findItem(R.id.save_subscribe);
MenuItem addToReadingList = aMenu.findItem(R.id.reading_list_add);
MenuItem save = aMenu.findItem(R.id.save);
@ -2383,7 +2384,7 @@ abstract public class BrowserApp extends GeckoApp
return true;
}
if (itemId == R.id.subscribe) {
if (itemId == R.id.subscribe || itemId == R.id.save_subscribe) {
subscribeToFeeds(tab);
return true;
}
@ -2615,9 +2616,8 @@ abstract public class BrowserApp extends GeckoApp
public int getLayout() { return R.layout.gecko_app; }
@Override
protected String getDefaultProfileName() {
String profile = GeckoProfile.findDefaultProfile(this);
return (profile != null ? profile : GeckoProfile.DEFAULT_PROFILE);
protected String getDefaultProfileName() throws NoMozillaDirectoryException {
return GeckoProfile.getDefaultProfileName(this);
}
/**

View File

@ -30,6 +30,7 @@ import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.background.announcements.AnnouncementsBroadcastService;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.favicons.Favicons;
@ -208,7 +209,7 @@ public abstract class GeckoApp
abstract public int getLayout();
abstract public boolean hasTabsSideBar();
abstract protected String getDefaultProfileName();
abstract protected String getDefaultProfileName() throws NoMozillaDirectoryException;
private static final String RESTARTER_ACTION = "org.mozilla.gecko.restart";
private static final String RESTARTER_CLASS = "org.mozilla.gecko.Restarter";
@ -1199,7 +1200,15 @@ public abstract class GeckoApp
profilePath = m.group(1);
}
if (profileName == null) {
profileName = getDefaultProfileName();
try {
profileName = getDefaultProfileName();
} catch (NoMozillaDirectoryException e) {
Log.wtf(LOGTAG, "Unable to fetch default profile name!", e);
// There's nothing at all we can do now. If the Mozilla directory
// didn't exist, then we're screwed.
// Crash here so we can fix the bug.
throw new RuntimeException(e);
}
if (profileName == null)
profileName = GeckoProfile.DEFAULT_PROFILE;
}

View File

@ -5,6 +5,8 @@
package org.mozilla.gecko;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
import org.mozilla.gecko.util.INIParser;
import org.mozilla.gecko.util.INISection;
@ -24,6 +26,7 @@ import java.util.Hashtable;
public final class GeckoProfile {
private static final String LOGTAG = "GeckoProfile";
// Used to "lock" the guest profile, so that we'll always restart in it
private static final String LOCK_FILE_NAME = ".active_lock";
public static final String DEFAULT_PROFILE = "default";
@ -31,9 +34,10 @@ public final class GeckoProfile {
private static HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>();
private static String sDefaultProfileName = null;
private final String mName;
private File mProfileDir;
public static boolean sIsUsingCustomProfile = false;
private final String mName;
private final File mMozillaDir;
private File mProfileDir; // Not final because this is lazily computed.
// Constants to cache whether or not a profile is "locked".
private enum LockState {
@ -41,36 +45,30 @@ public final class GeckoProfile {
UNLOCKED,
UNDEFINED
};
// Caches whether or not a profile is "locked". Only used by the guest profile to determine if it should
// be reused or deleted on startup
private LockState mLocked = LockState.UNDEFINED;
// Caches the guest profile dir
private static File mGuestDir = null;
// Caches the guest profile dir.
private static File sGuestDir = null;
private static GeckoProfile sGuestProfile = null;
private boolean mInGuestMode = false;
private static GeckoProfile mGuestProfile = null;
private static final String MOZILLA_DIR_NAME = "mozilla";
private static File sMozillaDir;
private static INIParser getProfilesINI(File mozillaDir) {
File profilesIni = new File(mozillaDir, "profiles.ini");
return new INIParser(profilesIni);
}
public static GeckoProfile get(Context context) {
boolean isGeckoApp = false;
try {
isGeckoApp = context instanceof GeckoApp;
} catch (NoClassDefFoundError ex) {}
if (isGeckoApp) {
// Check for a cached profile on this context already
// TODO: We should not be caching profile information on the Activity context
if (((GeckoApp)context).mProfile != null) {
return ((GeckoApp)context).mProfile;
final GeckoApp geckoApp = (GeckoApp) context;
if (geckoApp.mProfile != null) {
return geckoApp.mProfile;
}
}
@ -81,8 +79,18 @@ public final class GeckoProfile {
}
if (isGeckoApp) {
// Otherwise, get the default profile for the Activity
return get(context, ((GeckoApp)context).getDefaultProfileName());
final GeckoApp geckoApp = (GeckoApp) context;
String defaultProfileName;
try {
defaultProfileName = geckoApp.getDefaultProfileName();
} catch (NoMozillaDirectoryException e) {
// If this failed, we're screwed. But there are so many callers that
// we'll just throw a RuntimeException.
Log.wtf(LOGTAG, "Unable to get default profile name.", e);
throw new RuntimeException(e);
}
// Otherwise, get the default profile for the Activity.
return get(context, defaultProfileName);
}
return get(context, "");
@ -112,16 +120,24 @@ public final class GeckoProfile {
// if no profile was passed in, look for the default profile listed in profiles.ini
// if that doesn't exist, look for a profile called 'default'
if (TextUtils.isEmpty(profileName) && profileDir == null) {
profileName = GeckoProfile.findDefaultProfile(context);
if (profileName == null)
profileName = DEFAULT_PROFILE;
try {
profileName = GeckoProfile.getDefaultProfileName(context);
} catch (NoMozillaDirectoryException e) {
// We're unable to do anything sane here.
throw new RuntimeException(e);
}
}
// actually try to look up the profile
synchronized (sProfileCache) {
GeckoProfile profile = sProfileCache.get(profileName);
if (profile == null) {
profile = new GeckoProfile(context, profileName);
try {
profile = new GeckoProfile(context, profileName);
} catch (NoMozillaDirectoryException e) {
// We're unable to do anything sane here.
throw new RuntimeException(e);
}
profile.setDir(profileDir);
sProfileCache.put(profileName, profile);
} else {
@ -131,22 +147,13 @@ public final class GeckoProfile {
}
}
private static File getMozillaDirectory(Context context) {
return new File(context.getFilesDir(), MOZILLA_DIR_NAME);
}
private synchronized File ensureMozillaDirectory() throws IOException {
if (sMozillaDir.exists() || sMozillaDir.mkdirs()) {
return sMozillaDir;
}
// Although this leaks a path to the system log, the path is
// predictable (unlike a profile directory), so this is fine.
throw new IOException("Unable to create mozilla directory at " + sMozillaDir.getAbsolutePath());
}
public static boolean removeProfile(Context context, String profileName) {
return new GeckoProfile(context, profileName).remove();
try {
return new GeckoProfile(context, profileName).remove();
} catch (NoMozillaDirectoryException e) {
Log.w(LOGTAG, "Unable to remove profile: no Mozilla directory.", e);
return true;
}
}
public static GeckoProfile createGuestProfile(Context context) {
@ -172,22 +179,22 @@ public final class GeckoProfile {
}
private static File getGuestDir(Context context) {
if (mGuestDir == null) {
mGuestDir = context.getFileStreamPath("guest");
if (sGuestDir == null) {
sGuestDir = context.getFileStreamPath("guest");
}
return mGuestDir;
return sGuestDir;
}
private static GeckoProfile getGuestProfile(Context context) {
if (mGuestProfile == null) {
if (sGuestProfile == null) {
File guestDir = getGuestDir(context);
if (guestDir.exists()) {
mGuestProfile = get(context, "guest", guestDir);
mGuestProfile.mInGuestMode = true;
sGuestProfile = get(context, "guest", guestDir);
sGuestProfile.mInGuestMode = true;
}
}
return mGuestProfile;
return sGuestProfile;
}
public static boolean maybeCleanupGuestProfile(final Context context) {
@ -235,6 +242,11 @@ public final class GeckoProfile {
return file.delete();
}
private GeckoProfile(Context context, String profileName) throws NoMozillaDirectoryException {
mName = profileName;
mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
}
// Warning, Changing the lock file state from outside apis will cause this to become out of sync
public boolean locked() {
if (mLocked != LockState.UNDEFINED) {
@ -293,13 +305,6 @@ public final class GeckoProfile {
return false;
}
private GeckoProfile(Context context, String profileName) {
mName = profileName;
if (sMozillaDir == null) {
sMozillaDir = getMozillaDirectory(context);
}
}
public boolean inGuestMode() {
return mInGuestMode;
}
@ -328,13 +333,12 @@ public final class GeckoProfile {
try {
// Check if a profile with this name already exists.
File mozillaDir = ensureMozillaDirectory();
mProfileDir = findProfileDir(mozillaDir);
if (mProfileDir == null) {
// otherwise create it
mProfileDir = createProfileDir(mozillaDir);
} else {
try {
mProfileDir = findProfileDir();
Log.d(LOGTAG, "Found profile dir.");
} catch (NoSuchProfileException noSuchProfile) {
// If it doesn't exist, create it.
mProfileDir = createProfileDir();
}
} catch (IOException ioe) {
Log.e(LOGTAG, "Error getting profile dir", ioe);
@ -420,25 +424,27 @@ public final class GeckoProfile {
private boolean remove() {
try {
File dir = getDir();
if (dir.exists())
final File dir = getDir();
if (dir.exists()) {
delete(dir);
}
File mozillaDir = ensureMozillaDirectory();
mProfileDir = findProfileDir(mozillaDir);
if (mProfileDir == null) {
try {
mProfileDir = findProfileDir();
} catch (NoSuchProfileException noSuchProfile) {
// If the profile doesn't exist, there's nothing left for us to do.
return false;
}
INIParser parser = getProfilesINI(mozillaDir);
Hashtable<String, INISection> sections = parser.getSections();
final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
final Hashtable<String, INISection> sections = parser.getSections();
for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
INISection section = e.nextElement();
final INISection section = e.nextElement();
String name = section.getStringProperty("Name");
if (name == null || !name.equals(mName))
if (name == null || !name.equals(mName)) {
continue;
}
if (section.getName().startsWith("Profile")) {
// ok, we have stupid Profile#-named things. Rename backwards.
@ -452,7 +458,7 @@ public final class GeckoProfile {
while (sections.containsKey(nextSection)) {
parser.renameSection(nextSection, curSection);
sectionNumber++;
curSection = nextSection;
nextSection = "Profile" + (sectionNumber+1);
}
@ -476,7 +482,15 @@ public final class GeckoProfile {
}
}
public static String findDefaultProfile(Context context) {
/**
* @return the default profile name for this application, or
* {@link GeckoProfile#DEFAULT_PROFILE} if none could be found.
*
* @throws NoMozillaDirectoryException
* if the Mozilla directory did not exist and could not be
* created.
*/
public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
// Have we read the default profile from the INI already?
// Changing the default profile requires a restart, so we don't
// need to worry about runtime changes.
@ -484,58 +498,30 @@ public final class GeckoProfile {
return sDefaultProfileName;
}
// Open profiles.ini to find the correct path
INIParser parser = getProfilesINI(getMozillaDirectory(context));
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
INISection section = e.nextElement();
if (section.getIntProperty("Default") == 1) {
sDefaultProfileName = section.getStringProperty("Name");
return sDefaultProfileName;
}
final String profileName = GeckoProfileDirectories.findDefaultProfileName(context);
if (profileName == null) {
// Note that we don't persist this back to profiles.ini.
sDefaultProfileName = DEFAULT_PROFILE;
return DEFAULT_PROFILE;
}
return null;
sDefaultProfileName = profileName;
return sDefaultProfileName;
}
private File findProfileDir(File mozillaDir) {
// Open profiles.ini to find the correct path
INIParser parser = getProfilesINI(mozillaDir);
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
INISection section = e.nextElement();
String name = section.getStringProperty("Name");
if (name != null && name.equals(mName)) {
if (section.getIntProperty("IsRelative") == 1) {
return new File(mozillaDir, section.getStringProperty("Path"));
}
return new File(section.getStringProperty("Path"));
}
}
return null;
private File findProfileDir() throws NoSuchProfileException {
return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
}
private static String saltProfileName(String name) {
String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder salt = new StringBuilder(16);
for (int i = 0; i < 8; i++) {
salt.append(allowedChars.charAt((int)(Math.random() * allowedChars.length())));
}
salt.append('.');
salt.append(name);
return salt.toString();
}
private File createProfileDir(File mozillaDir) throws IOException {
INIParser parser = getProfilesINI(mozillaDir);
private File createProfileDir() throws IOException {
INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
// Salt the name of our requested profile
String saltedName = saltProfileName(mName);
File profileDir = new File(mozillaDir, saltedName);
String saltedName = GeckoProfileDirectories.saltProfileName(mName);
File profileDir = new File(mMozillaDir, saltedName);
while (profileDir.exists()) {
saltedName = saltProfileName(mName);
profileDir = new File(mozillaDir, saltedName);
saltedName = GeckoProfileDirectories.saltProfileName(mName);
profileDir = new File(mMozillaDir, saltedName);
}
// Attempt to create the salted profile dir

View File

@ -0,0 +1,229 @@
/* 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;
import java.io.File;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.util.INIParser;
import org.mozilla.gecko.util.INISection;
import android.content.Context;
/**
* <code>GeckoProfileDirectories</code> manages access to mappings from profile
* names to salted profile directory paths, as well as the default profile name.
*
* This class will eventually come to encapsulate the remaining logic embedded
* in profiles.ini; for now it's a read-only wrapper.
*/
public class GeckoProfileDirectories {
@SuppressWarnings("serial")
public static class NoMozillaDirectoryException extends Exception {
public NoMozillaDirectoryException(Throwable cause) {
super(cause);
}
public NoMozillaDirectoryException(String reason) {
super(reason);
}
public NoMozillaDirectoryException(String reason, Throwable cause) {
super(reason, cause);
}
}
@SuppressWarnings("serial")
public static class NoSuchProfileException extends Exception {
public NoSuchProfileException(String detailMessage, Throwable cause) {
super(detailMessage, cause);
}
public NoSuchProfileException(String detailMessage) {
super(detailMessage);
}
}
private interface INISectionPredicate {
public boolean matches(INISection section);
}
private static final String MOZILLA_DIR_NAME = "mozilla";
/**
* Returns true if the supplied profile entry represents the default profile.
*/
private static INISectionPredicate sectionIsDefault = new INISectionPredicate() {
@Override
public boolean matches(INISection section) {
return section.getIntProperty("Default") == 1;
}
};
/**
* Returns true if the supplied profile entry has a 'Name' field.
*/
private static INISectionPredicate sectionHasName = new INISectionPredicate() {
@Override
public boolean matches(INISection section) {
final String name = section.getStringProperty("Name");
return name != null;
}
};
/**
* Package-scoped because GeckoProfile needs to dig into this in order to do writes.
* This will be fixed in Bug 975212.
*/
static INIParser getProfilesINI(File mozillaDir) {
return new INIParser(new File(mozillaDir, "profiles.ini"));
}
/**
* Utility method to compute a salted profile name: eight random alphanumeric
* characters, followed by a period, followed by the profile name.
*/
public static String saltProfileName(final String name) {
if (name == null) {
throw new IllegalArgumentException("Cannot salt null profile name.");
}
final String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789";
final int scale = allowedChars.length();
final int saltSize = 8;
final StringBuilder saltBuilder = new StringBuilder(saltSize + 1 + name.length());
for (int i = 0; i < saltSize; i++) {
saltBuilder.append(allowedChars.charAt((int)(Math.random() * scale)));
}
saltBuilder.append('.');
saltBuilder.append(name);
return saltBuilder.toString();
}
/**
* Return the Mozilla directory within the files directory of the provided
* context. This should always be the same within a running application.
*
* This method is package-scoped so that new {@link GeckoProfile} instances can
* contextualize themselves.
*
* @return a new File object for the Mozilla directory.
* @throws NoMozillaDirectoryException
* if the directory did not exist and could not be created.
*/
static File getMozillaDirectory(Context context) throws NoMozillaDirectoryException {
final File mozillaDir = new File(context.getFilesDir(), MOZILLA_DIR_NAME);
if (mozillaDir.exists() || mozillaDir.mkdirs()) {
return mozillaDir;
}
// Although this leaks a path to the system log, the path is
// predictable (unlike a profile directory), so this is fine.
throw new NoMozillaDirectoryException("Unable to create mozilla directory at " + mozillaDir.getAbsolutePath());
}
/**
* Discover the default profile name by examining profiles.ini.
*
* Package-scoped because {@link GeckoProfile} needs access to it.
*
* @return null if there is no "Default" entry in profiles.ini, or the profile
* name if there is.
* @throws NoMozillaDirectoryException
* if the Mozilla directory did not exist and could not be created.
*/
static String findDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
final INIParser parser = GeckoProfileDirectories.getProfilesINI(getMozillaDirectory(context));
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
final INISection section = e.nextElement();
if (section.getIntProperty("Default") == 1) {
return section.getStringProperty("Name");
}
}
return null;
}
static Map<String, String> getDefaultProfile(final File mozillaDir) {
return getMatchingProfiles(mozillaDir, sectionIsDefault, true);
}
static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) {
final INISectionPredicate predicate = new INISectionPredicate() {
@Override
public boolean matches(final INISection section) {
return name.equals(section.getStringProperty("Name"));
}
};
return getMatchingProfiles(mozillaDir, predicate, true);
}
/**
* Calls {@link GeckoProfileDirectories#getMatchingProfiles(File, INISectionPredicate, boolean)}
* with a filter to ensure that all profiles are named.
*/
static Map<String, String> getAllProfiles(final File mozillaDir) {
return getMatchingProfiles(mozillaDir, sectionHasName, false);
}
/**
* Return a mapping from the names of all matching profiles (that is,
* profiles appearing in profiles.ini that match the supplied predicate) to
* their absolute paths on disk.
*
* @param mozillaDir
* a directory containing profiles.ini.
* @param predicate
* a predicate to use when evaluating whether to include a
* particular INI section.
* @param stopOnSuccess
* if true, this method will return with the first result that
* matches the predicate; if false, all matching results are
* included.
* @return a {@link Map} from name to path.
*/
public static Map<String, String> getMatchingProfiles(final File mozillaDir, INISectionPredicate predicate, boolean stopOnSuccess) {
final HashMap<String, String> result = new HashMap<String, String>();
final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
final INISection section = e.nextElement();
if (predicate == null || predicate.matches(section)) {
final String name = section.getStringProperty("Name");
final String pathString = section.getStringProperty("Path");
final boolean isRelative = section.getIntProperty("IsRelative") == 1;
final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
result.put(name, path.getAbsolutePath());
if (stopOnSuccess) {
return result;
}
}
}
return result;
}
public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException {
// Open profiles.ini to find the correct path.
final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
final INISection section = e.nextElement();
final String name = section.getStringProperty("Name");
if (name != null && name.equals(profileName)) {
if (section.getIntProperty("IsRelative") == 1) {
return new File(mozillaDir, section.getStringProperty("Path"));
}
return new File(section.getStringProperty("Path"));
}
}
throw new NoSuchProfileException("No profile " + profileName);
}
}

View File

@ -534,6 +534,8 @@ sync_java_files = [
'background/healthreport/upload/ObsoleteDocumentTracker.java',
'background/healthreport/upload/SubmissionClient.java',
'background/healthreport/upload/SubmissionPolicy.java',
'background/preferences/PreferenceFragment.java',
'background/preferences/PreferenceManagerCompat.java',
'browserid/ASNUtils.java',
'browserid/BrowserIDKeyPair.java',
'browserid/DSACryptoImplementation.java',
@ -550,12 +552,12 @@ sync_java_files = [
'fxa/activities/FxAccountAbstractSetupActivity.java',
'fxa/activities/FxAccountConfirmAccountActivity.java',
'fxa/activities/FxAccountCreateAccountActivity.java',
'fxa/activities/FxAccountCreateAccountFragment.java',
'fxa/activities/FxAccountCreateAccountNotAllowedActivity.java',
'fxa/activities/FxAccountGetStartedActivity.java',
'fxa/activities/FxAccountSetupTask.java',
'fxa/activities/FxAccountSignInActivity.java',
'fxa/activities/FxAccountStatusActivity.java',
'fxa/activities/FxAccountStatusFragment.java',
'fxa/activities/FxAccountUpdateCredentialsActivity.java',
'fxa/activities/FxAccountVerifiedAccountActivity.java',
'fxa/authenticator/AndroidFxAccount.java',
@ -576,6 +578,7 @@ sync_java_files = [
'fxa/login/StateFactory.java',
'fxa/login/TokensAndKeysState.java',
'fxa/sync/FxAccountGlobalSession.java',
'fxa/sync/FxAccountNotificationManager.java',
'fxa/sync/FxAccountSchedulePolicy.java',
'fxa/sync/FxAccountSyncAdapter.java',
'fxa/sync/FxAccountSyncService.java',

View File

@ -0,0 +1,318 @@
/*
* Copyright (C) 2013 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.background.preferences;
import org.mozilla.gecko.R;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.support.v4.app.Fragment;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.widget.ListView;
public abstract class PreferenceFragment extends Fragment implements PreferenceManagerCompat.OnPreferenceTreeClickListener {
private static final String PREFERENCES_TAG = "android:preferences";
private PreferenceManager mPreferenceManager;
private ListView mList;
private boolean mHavePrefs;
private boolean mInitDone;
/**
* The starting request code given out to preference framework.
*/
private static final int FIRST_REQUEST_CODE = 100;
private static final int MSG_BIND_PREFERENCES = 1;
// This triggers "This Handler class should be static or leaks might occur".
// The issue is that the Handler references the Fragment; messages targeting
// the Handler reference it; and if such messages are long lived, the Fragment
// cannot be GCed. This is not an issue for us; our messages are short-lived.
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_BIND_PREFERENCES:
bindPreferences();
break;
}
}
};
final private Runnable mRequestFocus = new Runnable() {
@Override
public void run() {
mList.focusableViewAvailable(mList);
}
};
/**
* Interface that PreferenceFragment's containing activity should
* implement to be able to process preference items that wish to
* switch to a new fragment.
*/
public interface OnPreferenceStartFragmentCallback {
/**
* Called when the user has clicked on a Preference that has
* a fragment class name associated with it. The implementation
* to should instantiate and switch to an instance of the given
* fragment.
*/
boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref);
}
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
mPreferenceManager = PreferenceManagerCompat.newInstance(getActivity(), FIRST_REQUEST_CODE);
PreferenceManagerCompat.setFragment(mPreferenceManager, this);
}
@Override
public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) {
return paramLayoutInflater.inflate(R.layout.fxaccount_preference_list_fragment, paramViewGroup,
false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mHavePrefs) {
bindPreferences();
}
mInitDone = true;
if (savedInstanceState != null) {
Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
if (container != null) {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.restoreHierarchyState(container);
}
}
}
}
@Override
public void onStart() {
super.onStart();
PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, this);
}
@Override
public void onStop() {
super.onStop();
PreferenceManagerCompat.dispatchActivityStop(mPreferenceManager);
PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, null);
}
@Override
public void onDestroyView() {
mList = null;
mHandler.removeCallbacks(mRequestFocus);
mHandler.removeMessages(MSG_BIND_PREFERENCES);
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
PreferenceManagerCompat.dispatchActivityDestroy(mPreferenceManager);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
Bundle container = new Bundle();
preferenceScreen.saveHierarchyState(container);
outState.putBundle(PREFERENCES_TAG, container);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
PreferenceManagerCompat.dispatchActivityResult(mPreferenceManager, requestCode, resultCode, data);
}
/**
* Returns the {@link PreferenceManager} used by this fragment.
* @return The {@link PreferenceManager}.
*/
public PreferenceManager getPreferenceManager() {
return mPreferenceManager;
}
/**
* Sets the root of the preference hierarchy that this fragment is showing.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
*/
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
if (PreferenceManagerCompat.setPreferences(mPreferenceManager, preferenceScreen) && preferenceScreen != null) {
mHavePrefs = true;
if (mInitDone) {
postBindPreferences();
}
}
}
/**
* Gets the root of the preference hierarchy that this fragment is showing.
*
* @return The {@link PreferenceScreen} that is the root of the preference
* hierarchy.
*/
public PreferenceScreen getPreferenceScreen() {
return PreferenceManagerCompat.getPreferenceScreen(mPreferenceManager);
}
/**
* Adds preferences from activities that match the given {@link Intent}.
*
* @param intent The {@link Intent} to query activities.
*/
public void addPreferencesFromIntent(Intent intent) {
requirePreferenceManager();
setPreferenceScreen(PreferenceManagerCompat.inflateFromIntent(mPreferenceManager, intent, getPreferenceScreen()));
}
/**
* Inflates the given XML resource and adds the preference hierarchy to the current
* preference hierarchy.
*
* @param preferencesResId The XML resource ID to inflate.
*/
public void addPreferencesFromResource(int preferencesResId) {
requirePreferenceManager();
setPreferenceScreen(PreferenceManagerCompat.inflateFromResource(mPreferenceManager, getActivity(),
preferencesResId, getPreferenceScreen()));
}
/**
* {@inheritDoc}
*/
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
Preference preference) {
//if (preference.getFragment() != null &&
if (
getActivity() instanceof OnPreferenceStartFragmentCallback) {
return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment(
this, preference);
}
return false;
}
/**
* Finds a {@link Preference} based on its key.
*
* @param key The key of the preference to retrieve.
* @return The {@link Preference} with the key, or null.
* @see PreferenceGroup#findPreference(CharSequence)
*/
public Preference findPreference(CharSequence key) {
if (mPreferenceManager == null) {
return null;
}
return mPreferenceManager.findPreference(key);
}
private void requirePreferenceManager() {
if (mPreferenceManager == null) {
throw new RuntimeException("This should be called after super.onCreate.");
}
}
private void postBindPreferences() {
if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
}
private void bindPreferences() {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.bind(getListView());
}
}
public ListView getListView() {
ensureList();
return mList;
}
private void ensureList() {
if (mList != null) {
return;
}
View root = getView();
if (root == null) {
throw new IllegalStateException("Content view not yet created");
}
View rawListView = root.findViewById(android.R.id.list);
if (!(rawListView instanceof ListView)) {
throw new RuntimeException(
"Content has view with id attribute 'android.R.id.list' "
+ "that is not a ListView class");
}
mList = (ListView)rawListView;
if (mList == null) {
throw new RuntimeException(
"Your content must have a ListView whose id attribute is " +
"'android.R.id.list'");
}
mList.setOnKeyListener(mListOnKeyListener);
mHandler.post(mRequestFocus);
}
private OnKeyListener mListOnKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
Object selectedItem = mList.getSelectedItem();
if (selectedItem instanceof Preference) {
@SuppressWarnings("unused")
View selectedView = mList.getSelectedView();
//return ((Preference)selectedItem).onKey(
// selectedView, keyCode, event);
return false;
}
return false;
}
};
}

View File

@ -0,0 +1,231 @@
/*
* Copyright (C) 2013 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.background.preferences;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.Log;
public class PreferenceManagerCompat {
private static final String TAG = PreferenceManagerCompat.class.getSimpleName();
/**
* Interface definition for a callback to be invoked when a
* {@link Preference} in the hierarchy rooted at this {@link PreferenceScreen} is
* clicked.
*/
interface OnPreferenceTreeClickListener {
/**
* Called when a preference in the tree rooted at this
* {@link PreferenceScreen} has been clicked.
*
* @param preferenceScreen The {@link PreferenceScreen} that the
* preference is located in.
* @param preference The preference that was clicked.
* @return Whether the click was handled.
*/
boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference);
}
static PreferenceManager newInstance(Activity activity, int firstRequestCode) {
try {
Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
c.setAccessible(true);
return c.newInstance(activity, firstRequestCode);
} catch (Exception e) {
Log.w(TAG, "Couldn't call constructor PreferenceManager by reflection", e);
}
return null;
}
/**
* Sets the owning preference fragment
*/
static void setFragment(PreferenceManager manager, PreferenceFragment fragment) {
// stub
}
/**
* Sets the callback to be invoked when a {@link Preference} in the
* hierarchy rooted at this {@link PreferenceManager} is clicked.
*
* @param listener The callback to be invoked.
*/
static void setOnPreferenceTreeClickListener(PreferenceManager manager, final OnPreferenceTreeClickListener listener) {
try {
Field onPreferenceTreeClickListener = PreferenceManager.class.getDeclaredField("mOnPreferenceTreeClickListener");
onPreferenceTreeClickListener.setAccessible(true);
if (listener != null) {
Object proxy = Proxy.newProxyInstance(
onPreferenceTreeClickListener.getType().getClassLoader(),
new Class<?>[] { onPreferenceTreeClickListener.getType() },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
if (method.getName().equals("onPreferenceTreeClick")) {
return Boolean.valueOf(listener.onPreferenceTreeClick((PreferenceScreen) args[0], (Preference) args[1]));
} else {
return null;
}
}
});
onPreferenceTreeClickListener.set(manager, proxy);
} else {
onPreferenceTreeClickListener.set(manager, null);
}
} catch (Exception e) {
Log.w(TAG, "Couldn't set PreferenceManager.mOnPreferenceTreeClickListener by reflection", e);
}
}
/**
* Inflates a preference hierarchy from the preference hierarchies of
* {@link Activity Activities} that match the given {@link Intent}. An
* {@link Activity} defines its preference hierarchy with meta-data using
* the {@link #METADATA_KEY_PREFERENCES} key.
* <p>
* If a preference hierarchy is given, the new preference hierarchies will
* be merged in.
*
* @param queryIntent The intent to match activities.
* @param rootPreferences Optional existing hierarchy to merge the new
* hierarchies into.
* @return The root hierarchy (if one was not provided, the new hierarchy's
* root).
*/
static PreferenceScreen inflateFromIntent(PreferenceManager manager, Intent intent, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, intent, screen);
return prefScreen;
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.inflateFromIntent by reflection", e);
}
return null;
}
/**
* Inflates a preference hierarchy from XML. If a preference hierarchy is
* given, the new preference hierarchies will be merged in.
*
* @param context The context of the resource.
* @param resId The resource ID of the XML to inflate.
* @param rootPreferences Optional existing hierarchy to merge the new
* hierarchies into.
* @return The root hierarchy (if one was not provided, the new hierarchy's
* root).
* @hide
*/
static PreferenceScreen inflateFromResource(PreferenceManager manager, Activity activity, int resId, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, activity, resId, screen);
return prefScreen;
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.inflateFromResource by reflection", e);
}
return null;
}
/**
* Returns the root of the preference hierarchy managed by this class.
*
* @return The {@link PreferenceScreen} object that is at the root of the hierarchy.
*/
static PreferenceScreen getPreferenceScreen(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
m.setAccessible(true);
return (PreferenceScreen) m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.getPreferenceScreen by reflection", e);
}
return null;
}
/**
* Called by the {@link PreferenceManager} to dispatch a subactivity result.
*/
static void dispatchActivityResult(PreferenceManager manager, int requestCode, int resultCode, Intent data) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
m.setAccessible(true);
m.invoke(manager, requestCode, resultCode, data);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityResult by reflection", e);
}
}
/**
* Called by the {@link PreferenceManager} to dispatch the activity stop
* event.
*/
static void dispatchActivityStop(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
m.setAccessible(true);
m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityStop by reflection", e);
}
}
/**
* Called by the {@link PreferenceManager} to dispatch the activity destroy
* event.
*/
static void dispatchActivityDestroy(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
m.setAccessible(true);
m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityDestroy by reflection", e);
}
}
/**
* Sets the root of the preference hierarchy.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
* @return Whether the {@link PreferenceScreen} given is different than the previous.
*/
static boolean setPreferences(PreferenceManager manager, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
m.setAccessible(true);
return ((Boolean) m.invoke(manager, screen));
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.setPreferences by reflection", e);
}
return false;
}
}

View File

@ -28,6 +28,8 @@ import android.content.Intent;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
import android.text.method.SingleLineTransformationMethod;
import android.util.Patterns;
import android.view.KeyEvent;
import android.view.View;
@ -64,21 +66,24 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
@SuppressWarnings("deprecation")
@Override
public void onClick(View v) {
boolean isShown = 0 == (passwordEdit.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD);
boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod;
// Changing input type loses position in edit text; let's try to maintain it.
int start = passwordEdit.getSelectionStart();
int stop = passwordEdit.getSelectionEnd();
passwordEdit.setInputType(passwordEdit.getInputType() ^ InputType.TYPE_TEXT_VARIATION_PASSWORD);
passwordEdit.setSelection(start, stop);
if (isShown) {
passwordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance());
showPasswordButton.setText(R.string.fxaccount_password_show);
showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background));
showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_show_textcolor));
} else {
passwordEdit.setTransformationMethod(SingleLineTransformationMethod.getInstance());
showPasswordButton.setText(R.string.fxaccount_password_hide);
showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_hide_background));
showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_hide_textcolor));
}
passwordEdit.setSelection(start, stop);
}
});
}

View File

@ -4,375 +4,103 @@
package org.mozilla.gecko.fxa.activities;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import android.content.ContentResolver;
import android.content.SharedPreferences;
import android.accounts.Account;
import android.annotation.TargetApi;
import android.app.ActionBar;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.ViewFlipper;
import android.support.v4.app.FragmentActivity;
import android.view.MenuItem;
/**
* Activity which displays account status.
*/
public class FxAccountStatusActivity extends FxAccountAbstractActivity implements OnClickListener {
public class FxAccountStatusActivity extends FragmentActivity {
private static final String LOG_TAG = FxAccountStatusActivity.class.getSimpleName();
// When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
// before trying to sync. Should we quit the activity before the sync request
// happens, that's okay: the runnable will run if the UI thread is still
// around to service it, and since we're not updating any UI, we'll just
// schedule the sync as usual. See also comment below about garbage
// collection.
private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
protected FxAccountStatusFragment statusFragment;
// Set in onCreate.
protected TextView syncStatusTextView;
protected ViewFlipper connectionStatusViewFlipper;
protected View connectionStatusUnverifiedView;
protected View connectionStatusSignInView;
protected TextView emailTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
protected CheckBox bookmarksCheckBox;
protected CheckBox historyCheckBox;
protected CheckBox passwordsCheckBox;
protected CheckBox tabsCheckBox;
// Display the fragment as the content.
statusFragment = new FxAccountStatusFragment();
getSupportFragmentManager()
.beginTransaction()
.replace(android.R.id.content, statusFragment)
.commit();
// Used to post delayed sync requests.
protected Handler handler;
// Set in onResume.
protected AndroidFxAccount fxAccount;
// Member variable so that re-posting pushes back the already posted instance.
// This Runnable references the fxAccount above, so it is not specific a
// single account. (That is, it does not capture a single account instance.)
protected Runnable requestSyncRunnable;
public FxAccountStatusActivity() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
maybeSetHomeButtonEnabled();
}
/**
* {@inheritDoc}
* Sufficiently recent Android versions need additional code to receive taps
* on the status bar to go "up". See <a
* href="http://stackoverflow.com/a/8953148">this stackoverflow answer</a> for
* more information.
*/
@Override
public void onCreate(Bundle icicle) {
Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
super.onCreate(icicle);
setContentView(R.layout.fxaccount_status);
syncStatusTextView = (TextView) ensureFindViewById(null, R.id.sync_status_text, "sync status text");
connectionStatusViewFlipper = (ViewFlipper) ensureFindViewById(null, R.id.connection_status_view, "connection status frame layout");
connectionStatusUnverifiedView = ensureFindViewById(null, R.id.unverified_view, "unverified view");
connectionStatusSignInView = ensureFindViewById(null, R.id.sign_in_view, "sign in view");
launchActivityOnClick(connectionStatusSignInView, FxAccountUpdateCredentialsActivity.class);
connectionStatusUnverifiedView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
FxAccountConfirmAccountActivity.resendCode(getApplicationContext(), fxAccount);
launchActivity(FxAccountConfirmAccountActivity.class);
}
});
emailTextView = (TextView) findViewById(R.id.email);
bookmarksCheckBox = (CheckBox) findViewById(R.id.bookmarks_checkbox);
historyCheckBox = (CheckBox) findViewById(R.id.history_checkbox);
passwordsCheckBox = (CheckBox) findViewById(R.id.passwords_checkbox);
tabsCheckBox = (CheckBox) findViewById(R.id.tabs_checkbox);
bookmarksCheckBox.setOnClickListener(this);
historyCheckBox.setOnClickListener(this);
passwordsCheckBox.setOnClickListener(this);
tabsCheckBox.setOnClickListener(this);
handler = new Handler(); // Attached to current (UI) thread.
// Runnable is not specific to one Firefox Account. This runnable will keep
// a reference to this activity alive, but we expect posted runnables to be
// serviced very quickly, so this is not an issue.
requestSyncRunnable = new Runnable() {
@Override
public void run() {
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
// Request a sync, but not necessarily an immediate sync.
ContentResolver.requestSync(fxAccount.getAndroidAccount(), BrowserContract.AUTHORITY, Bundle.EMPTY);
// SyncAdapter.requestImmediateSync(fxAccount.getAndroidAccount(), null);
}
};
TextView privacyView = (TextView) findViewById(R.id.fxaccount_status_linkprivacy);
ActivityUtils.linkTextView(privacyView, R.string.fxaccount_policy_linkprivacy, R.string.fxaccount_link_pn);
TextView tosView = (TextView) findViewById(R.id.fxaccount_status_linktos);
ActivityUtils.linkTextView(tosView, R.string.fxaccount_policy_linktos, R.string.fxaccount_link_tos);
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
createDebugButtons();
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected void maybeSetHomeButtonEnabled() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
Logger.debug(LOG_TAG, "Not enabling home button; version too low.");
return;
}
final ActionBar actionBar = getActionBar();
if (actionBar != null) {
Logger.debug(LOG_TAG, "Enabling home button.");
actionBar.setHomeButtonEnabled(true);
return;
}
Logger.debug(LOG_TAG, "Not enabling home button.");
}
@Override
public void onResume() {
super.onResume();
this.fxAccount = getAndroidFxAccount();
final AndroidFxAccount fxAccount = getAndroidFxAccount();
if (fxAccount == null) {
Logger.warn(LOG_TAG, "Could not get Firefox Account.");
// Gracefully redirect to get started.
Intent intent = new Intent(this, FxAccountGetStartedActivity.class);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
setResult(RESULT_CANCELED);
finish();
return;
}
refresh();
statusFragment.refresh(fxAccount);
}
protected void setCheckboxesEnabled(boolean enabled) {
bookmarksCheckBox.setEnabled(enabled);
historyCheckBox.setEnabled(enabled);
passwordsCheckBox.setEnabled(enabled);
tabsCheckBox.setEnabled(enabled);
}
protected void showNeedsUpgrade() {
syncStatusTextView.setText(R.string.fxaccount_status_sync);
connectionStatusViewFlipper.setVisibility(View.VISIBLE);
connectionStatusViewFlipper.setDisplayedChild(0);
setCheckboxesEnabled(false);
}
protected void showNeedsPassword() {
syncStatusTextView.setText(R.string.fxaccount_status_sync);
connectionStatusViewFlipper.setVisibility(View.VISIBLE);
connectionStatusViewFlipper.setDisplayedChild(1);
setCheckboxesEnabled(false);
}
protected void showNeedsVerification() {
syncStatusTextView.setText(R.string.fxaccount_status_sync);
connectionStatusViewFlipper.setVisibility(View.VISIBLE);
connectionStatusViewFlipper.setDisplayedChild(2);
setCheckboxesEnabled(false);
}
protected void showConnected() {
syncStatusTextView.setText(R.string.fxaccount_status_sync_enabled);
connectionStatusViewFlipper.setVisibility(View.GONE);
setCheckboxesEnabled(true);
}
protected void refresh() {
emailTextView.setText(fxAccount.getEmail());
// Interrogate the Firefox Account's state.
State state = fxAccount.getState();
switch (state.getNeededAction()) {
case NeedsUpgrade:
showNeedsUpgrade();
break;
case NeedsPassword:
showNeedsPassword();
break;
case NeedsVerification:
showNeedsVerification();
break;
default:
showConnected();
}
updateSelectedEngines();
}
protected void updateSelectedEngines() {
try {
SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
Map<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs);
if (engines != null) {
bookmarksCheckBox.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks"));
historyCheckBox.setChecked(engines.containsKey("history") && engines.get("history"));
passwordsCheckBox.setChecked(engines.containsKey("passwords") && engines.get("passwords"));
tabsCheckBox.setChecked(engines.containsKey("tabs") && engines.get("tabs"));
return;
}
// We don't have user specified preferences. Perhaps we have seen a meta/global?
Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs);
if (enabledNames != null) {
bookmarksCheckBox.setChecked(enabledNames.contains("bookmarks"));
historyCheckBox.setChecked(enabledNames.contains("history"));
passwordsCheckBox.setChecked(enabledNames.contains("passwords"));
tabsCheckBox.setChecked(enabledNames.contains("tabs"));
return;
}
// Okay, we don't have userSelectedEngines or enabledEngines. That means
// the user hasn't specified to begin with, we haven't specified here, and
// we haven't already seen, Sync engines. We don't know our state, so
// let's check everything (the default) and disable everything.
bookmarksCheckBox.setChecked(true);
historyCheckBox.setChecked(true);
passwordsCheckBox.setChecked(true);
tabsCheckBox.setChecked(true);
setCheckboxesEnabled(false);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e);
return;
/**
* Helper to fetch (unique) Android Firefox Account if one exists, or return null.
*/
protected AndroidFxAccount getAndroidFxAccount() {
Account account = FirefoxAccounts.getFirefoxAccount(this);
if (account == null) {
return null;
}
return new AndroidFxAccount(this, account);
}
@Override
public void onClick(View view) {
if (view == bookmarksCheckBox ||
view == historyCheckBox ||
view == passwordsCheckBox ||
view == tabsCheckBox) {
saveEngineSelections();
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
switch (itemId) {
case android.R.id.home:
finish();
return true;
}
}
protected void saveEngineSelections() {
Map<String, Boolean> engineSelections = new HashMap<String, Boolean>();
engineSelections.put("bookmarks", bookmarksCheckBox.isChecked());
engineSelections.put("history", historyCheckBox.isChecked());
engineSelections.put("passwords", passwordsCheckBox.isChecked());
engineSelections.put("tabs", tabsCheckBox.isChecked());
Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString());
try {
// No GlobalSession.config, so store directly to prefs. We'd like to do
// this on a background thread to avoid IO on the main thread and strict
// mode warnings, but all in good time.
SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections);
requestDelayedSync();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e);
return;
}
}
protected void requestDelayedSync() {
Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon.");
handler.removeCallbacks(requestSyncRunnable);
handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC);
}
protected void createDebugButtons() {
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
return;
}
final LinearLayout existingUserView = (LinearLayout) findViewById(R.id.existing_user);
if (existingUserView == null) {
return;
}
final LinearLayout debugButtonsView = new LinearLayout(this);
debugButtonsView.setOrientation(LinearLayout.VERTICAL);
debugButtonsView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
existingUserView.addView(debugButtonsView, existingUserView.getChildCount());
Button button;
button = new Button(this);
debugButtonsView.addView(button, debugButtonsView.getChildCount());
button.setText("Refresh status view");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Refreshing.");
refresh();
}
});
button = new Button(this);
debugButtonsView.addView(button, debugButtonsView.getChildCount());
button.setText("Dump account details");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
fxAccount.dump();
}
});
button = new Button(this);
debugButtonsView.addView(button, debugButtonsView.getChildCount());
button.setText("Force sync");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Syncing.");
final Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
ContentResolver.requestSync(fxAccount.getAndroidAccount(), BrowserContract.AUTHORITY, extras);
// No sense refreshing, since the sync will complete in the future.
}
});
button = new Button(this);
debugButtonsView.addView(button, debugButtonsView.getChildCount());
button.setText("Forget certificate (if applicable)");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
State state = fxAccount.getState();
try {
Married married = (Married) state;
Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
fxAccount.setState(married.makeCohabitingState());
refresh();
} catch (ClassCastException e) {
Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
// Ignore.
}
}
});
button = new Button(this);
debugButtonsView.addView(button, debugButtonsView.getChildCount());
button.setText("Require password");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
State state = fxAccount.getState();
fxAccount.setState(state.makeSeparatedState());
refresh();
}
});
button = new Button(this);
debugButtonsView.addView(button, debugButtonsView.getChildCount());
button.setText("Require upgrade");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
State state = fxAccount.getState();
fxAccount.setState(state.makeDoghouseState());
refresh();
}
});
return super.onOptionsItemSelected(item);
}
}

View File

@ -0,0 +1,468 @@
/* 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.fxa.activities;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.preferences.PreferenceFragment;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.SyncConfiguration;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
/**
* A fragment that displays the status of an AndroidFxAccount.
* <p>
* The owning activity is responsible for providing an AndroidFxAccount at
* appropriate times.
*/
public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener {
private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
// When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
// before trying to sync. Should we kill off the fragment before the sync
// request happens, that's okay: the runnable will run if the UI thread is
// still around to service it, and since we're not updating any UI, we'll just
// schedule the sync as usual. See also comment below about garbage
// collection.
private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
protected Preference emailPreference;
protected Preference needsPasswordPreference;
protected Preference needsUpgradePreference;
protected Preference needsVerificationPreference;
protected PreferenceCategory syncCategory;
protected CheckBoxPreference bookmarksPreference;
protected CheckBoxPreference historyPreference;
protected CheckBoxPreference tabsPreference;
protected CheckBoxPreference passwordsPreference;
protected volatile AndroidFxAccount fxAccount;
// Used to post delayed sync requests.
protected Handler handler;
// Member variable so that re-posting pushes back the already posted instance.
// This Runnable references the fxAccount above, but it is not specific to a
// single account. (That is, it does not capture a single account instance.)
protected Runnable requestSyncRunnable;
protected Preference ensureFindPreference(String key) {
Preference preference = findPreference(key);
if (preference == null) {
throw new IllegalStateException("Could not find preference with key: " + key);
}
return preference;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
emailPreference = ensureFindPreference("email");
needsPasswordPreference = ensureFindPreference("needs_credentials");
needsUpgradePreference = ensureFindPreference("needs_upgrade");
needsVerificationPreference = ensureFindPreference("needs_verification");
syncCategory = (PreferenceCategory) ensureFindPreference("sync_category");
bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
historyPreference = (CheckBoxPreference) ensureFindPreference("history");
tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
removeDebugButtons();
} else {
connectDebugButtons();
}
needsPasswordPreference.setOnPreferenceClickListener(this);
needsVerificationPreference.setOnPreferenceClickListener(this);
bookmarksPreference.setOnPreferenceClickListener(this);
historyPreference.setOnPreferenceClickListener(this);
tabsPreference.setOnPreferenceClickListener(this);
passwordsPreference.setOnPreferenceClickListener(this);
}
/**
* We intentionally don't refresh here. Our owning activity is responsible for
* providing an AndroidFxAccount to our refresh method in its onResume method.
*/
@Override
public void onResume() {
super.onResume();
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference == needsPasswordPreference) {
Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
return true;
}
if (preference == needsVerificationPreference) {
FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount);
Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
return true;
}
if (preference == bookmarksPreference ||
preference == historyPreference ||
preference == passwordsPreference ||
preference == tabsPreference) {
saveEngineSelections();
return true;
}
return false;
}
protected void setCheckboxesEnabled(boolean enabled) {
bookmarksPreference.setEnabled(enabled);
historyPreference.setEnabled(enabled);
tabsPreference.setEnabled(enabled);
passwordsPreference.setEnabled(enabled);
}
/**
* Show at most one error preference, hiding all others.
*
* @param errorPreferenceToShow
* single error preference to show; if null, hide all error preferences
*/
protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) {
final Preference[] errorPreferences = new Preference[] {
this.needsPasswordPreference,
this.needsUpgradePreference,
this.needsVerificationPreference };
for (Preference errorPreference : errorPreferences) {
final boolean currentlyShown = null != findPreference(errorPreference.getKey());
final boolean shouldBeShown = errorPreference == errorPreferenceToShow;
if (currentlyShown == shouldBeShown) {
continue;
}
if (shouldBeShown) {
syncCategory.addPreference(errorPreference);
} else {
syncCategory.removePreference(errorPreference);
}
}
}
protected void showNeedsPassword() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsPasswordPreference);
setCheckboxesEnabled(false);
}
protected void showNeedsUpgrade() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsUpgradePreference);
setCheckboxesEnabled(false);
}
protected void showNeedsVerification() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsVerificationPreference);
setCheckboxesEnabled(false);
}
protected void showConnected() {
syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
showOnlyOneErrorPreference(null);
setCheckboxesEnabled(true);
}
/**
* Notify the fragment that a new AndroidFxAccount instance is current.
* <p>
* <b>Important:</b> call this method on the UI thread!
* <p>
* In future, this might be a Loader.
*
* @param fxAccount new instance.
*/
public void refresh(AndroidFxAccount fxAccount) {
if (fxAccount == null) {
throw new IllegalArgumentException("fxAccount must not be null");
}
this.fxAccount = fxAccount;
handler = new Handler(); // Attached to current (assumed to be UI) thread.
// Runnable is not specific to one Firefox Account. This runnable will keep
// a reference to this fragment alive, but we expect posted runnables to be
// serviced very quickly, so this is not an issue.
requestSyncRunnable = new RequestSyncRunnable();
refresh();
}
protected void refresh() {
// refresh is called from our onResume, which can happen before the owning
// Activity tells us about an account (via our public
// refresh(AndroidFxAccount) method).
if (fxAccount == null) {
throw new IllegalArgumentException("fxAccount must not be null");
}
emailPreference.setTitle(fxAccount.getEmail());
// Interrogate the Firefox Account's state.
State state = fxAccount.getState();
switch (state.getNeededAction()) {
case NeedsUpgrade:
showNeedsUpgrade();
break;
case NeedsPassword:
showNeedsPassword();
break;
case NeedsVerification:
showNeedsVerification();
break;
default:
showConnected();
}
updateSelectedEngines();
}
/**
* Query shared prefs for the current engine state, and update the UI
* accordingly.
* <p>
* In future, we might want this to be on a background thread, or implemented
* as a Loader.
*/
protected void updateSelectedEngines() {
try {
SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
Map<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs);
if (engines != null) {
bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks"));
historyPreference.setChecked(engines.containsKey("history") && engines.get("history"));
passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords"));
tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs"));
return;
}
// We don't have user specified preferences. Perhaps we have seen a meta/global?
Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs);
if (enabledNames != null) {
bookmarksPreference.setChecked(enabledNames.contains("bookmarks"));
historyPreference.setChecked(enabledNames.contains("history"));
passwordsPreference.setChecked(enabledNames.contains("passwords"));
tabsPreference.setChecked(enabledNames.contains("tabs"));
return;
}
// Okay, we don't have userSelectedEngines or enabledEngines. That means
// the user hasn't specified to begin with, we haven't specified here, and
// we haven't already seen, Sync engines. We don't know our state, so
// let's check everything (the default) and disable everything.
bookmarksPreference.setChecked(true);
historyPreference.setChecked(true);
passwordsPreference.setChecked(true);
tabsPreference.setChecked(true);
setCheckboxesEnabled(false);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e);
return;
}
}
/**
* Persist engine selections to local shared preferences, and request a sync
* to persist selections to remote storage.
*/
protected void saveEngineSelections() {
final Map<String, Boolean> engineSelections = new HashMap<String, Boolean>();
engineSelections.put("bookmarks", bookmarksPreference.isChecked());
engineSelections.put("history", historyPreference.isChecked());
engineSelections.put("passwords", passwordsPreference.isChecked());
engineSelections.put("tabs", tabsPreference.isChecked());
// No GlobalSession.config, so store directly to shared prefs. We do this on
// a background thread to avoid IO on the main thread and strict mode
// warnings.
new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start();
}
protected void requestDelayedSync() {
Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon.");
handler.removeCallbacks(requestSyncRunnable);
handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC);
}
/**
* Remove all traces of debug buttons. By default, no debug buttons are shown.
*/
protected void removeDebugButtons() {
final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
statusScreen.removePreference(debugCategory);
}
/**
* A Runnable that persists engine selections to shared prefs, and then
* requests a delayed sync.
* <p>
* References the member <code>fxAccount</code> and is specific to the Android
* account associated to that account.
*/
protected class PersistEngineSelectionsRunnable implements Runnable {
private final Map<String, Boolean> engineSelections;
protected PersistEngineSelectionsRunnable(Map<String, Boolean> engineSelections) {
this.engineSelections = engineSelections;
}
@Override
public void run() {
try {
// Name shadowing -- do you like it, or do you love it?
AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString());
SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections);
requestDelayedSync();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e);
return;
}
}
}
/**
* A Runnable that requests a sync.
* <p>
* References the member <code>fxAccount</code>, but is not specific to the
* Android account associated to that account.
*/
protected class RequestSyncRunnable implements Runnable {
@Override
public void run() {
// Name shadowing -- do you like it, or do you love it?
AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
// Request a sync, but not necessarily an immediate sync.
ContentResolver.requestSync(fxAccount.getAndroidAccount(), BrowserContract.AUTHORITY, Bundle.EMPTY);
// SyncAdapter.requestImmediateSync(fxAccount.getAndroidAccount(), null);
}
}
/**
* A separate listener to separate debug logic from main code paths.
*/
protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
final String key = preference.getKey();
if ("debug_refresh".equals(key)) {
Logger.info(LOG_TAG, "Refreshing.");
refresh();
} else if ("debug_dump".equals(key)) {
fxAccount.dump();
} else if ("debug_force_sync".equals(key)) {
Logger.info(LOG_TAG, "Syncing.");
final Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
fxAccount.requestSync(extras);
// No sense refreshing, since the sync will complete in the future.
} else if ("debug_forget_certificate".equals(key)) {
State state = fxAccount.getState();
try {
Married married = (Married) state;
Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
fxAccount.setState(married.makeCohabitingState());
refresh();
} catch (ClassCastException e) {
Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
// Ignore.
}
} else if ("debug_require_password".equals(key)) {
Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
State state = fxAccount.getState();
fxAccount.setState(state.makeSeparatedState());
refresh();
} else if ("debug_require_upgrade".equals(key)) {
Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
State state = fxAccount.getState();
fxAccount.setState(state.makeDoghouseState());
refresh();
} else {
return false;
}
return true;
}
}
/**
* Iterate through debug buttons, adding a special deubg preference click
* listener to each of them.
*/
protected void connectDebugButtons() {
// Separate listener to really separate debug logic from main code paths.
final OnPreferenceClickListener listener = new DebugPreferenceClickListener();
// We don't want to use Android resource strings for debug UI, so we just
// use the keys throughout.
final Preference debugCategory = ensureFindPreference("debug_category");
debugCategory.setTitle(debugCategory.getKey());
String[] debugKeys = new String[] {
"debug_refresh",
"debug_dump",
"debug_force_sync",
"debug_forget_certificate",
"debug_require_password",
"debug_require_upgrade" };
for (String debugKey : debugKeys) {
final Preference button = ensureFindPreference(debugKey);
button.setTitle(debugKey); // Not very friendly, but this is for debugging only!
button.setOnPreferenceClickListener(listener);
}
}
}

View File

@ -11,7 +11,9 @@ import java.util.ArrayList;
import java.util.Collections;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
@ -21,6 +23,7 @@ import org.mozilla.gecko.sync.Utils;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
@ -324,17 +327,34 @@ public class AndroidFxAccount {
}
public void enableSyncing() {
FxAccountAuthenticator.enableSyncing(context, account);
Logger.info(LOG_TAG, "Disabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
for (String authority : new String[] { BrowserContract.AUTHORITY }) {
ContentResolver.setSyncAutomatically(account, authority, true);
ContentResolver.setIsSyncable(account, authority, 1);
}
}
public void disableSyncing() {
FxAccountAuthenticator.disableSyncing(context, account);
Logger.info(LOG_TAG, "Disabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
for (String authority : new String[] { BrowserContract.AUTHORITY }) {
ContentResolver.setSyncAutomatically(account, authority, false);
}
}
public void requestSync(Bundle extras) {
Logger.info(LOG_TAG, "Requesting sync for account named like " + Utils.obfuscateEmail(getEmail()) +
(extras.isEmpty() ? "." : "; has extras."));
for (String authority : new String[] { BrowserContract.AUTHORITY }) {
ContentResolver.requestSync(account, authority, extras);
}
}
public synchronized void setState(State state) {
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
Logger.info(LOG_TAG, "Moving account named like " + Utils.obfuscateEmail(getEmail()) +
" to state " + state.getStateLabel().toString());
updateBundleValue(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
updateBundleValue(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
}

View File

@ -4,7 +4,6 @@
package org.mozilla.gecko.fxa.authenticator;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
@ -14,7 +13,6 @@ import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@ -31,23 +29,6 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
this.accountManager = AccountManager.get(context);
}
protected static void enableSyncing(Context context, Account account) {
for (String authority : new String[] {
AppConstants.ANDROID_PACKAGE_NAME + ".db.browser",
}) {
ContentResolver.setSyncAutomatically(account, authority, true);
ContentResolver.setIsSyncable(account, authority, 1);
}
}
protected static void disableSyncing(Context context, Account account) {
for (String authority : new String[] {
AppConstants.ANDROID_PACKAGE_NAME + ".db.browser",
}) {
ContentResolver.setSyncAutomatically(account, authority, false);
}
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType, String authTokenType, String[] requiredFeatures,

View File

@ -0,0 +1,78 @@
/* 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.fxa.sync;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
/**
* Abstraction that manages notifications shown or hidden for a Firefox Account.
* <p>
* In future, we anticipate this tracking things like:
* <ul>
* <li>new engines to offer to Sync;</li>
* <li>service interruption updates;</li>
* <li>messages from other clients.</li>
* </ul>
*/
public class FxAccountNotificationManager {
private static final String LOG_TAG = FxAccountNotificationManager.class.getSimpleName();
protected final int notificationId;
public FxAccountNotificationManager(int notificationId) {
this.notificationId = notificationId;
}
/**
* Reflect new Firefox Account state to the notification manager: show or hide
* notifications reflecting the state of a Firefox Account.
*
* @param context
* Android context.
* @param fxAccount
* Firefox Account to reflect to the notification manager.
*/
protected void update(Context context, AndroidFxAccount fxAccount) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final State state = fxAccount.getState();
final Action action = state.getNeededAction();
if (action == Action.None) {
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs no action; cancelling any existing notification.");
notificationManager.cancel(notificationId);
return;
}
final String title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
final String text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
FxAccountConstants.pii(LOG_TAG, "And text: " + text);
final Intent notificationIntent = new Intent(context, FxAccountStatusActivity.class);
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
final Builder builder = new NotificationCompat.Builder(context);
builder
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(R.drawable.ic_status_logo)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
notificationManager.notify(notificationId, builder.build());
}
}

View File

@ -11,7 +11,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
@ -22,7 +21,6 @@ import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient;
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
@ -30,7 +28,6 @@ import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineD
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.BackoffHandler;
import org.mozilla.gecko.sync.ExtendedJSONObject;
@ -51,19 +48,14 @@ import org.mozilla.gecko.tokenserver.TokenServerException;
import org.mozilla.gecko.tokenserver.TokenServerToken;
import android.accounts.Account;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
@ -78,10 +70,12 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
private volatile long lastSyncRealtimeMillis = 0L;
protected final ExecutorService executor;
protected final FxAccountNotificationManager notificationManager;
public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
this.executor = Executors.newSingleThreadExecutor();
this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
}
protected static class SyncDelegate {
@ -89,8 +83,10 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
protected final CountDownLatch latch;
protected final SyncResult syncResult;
protected final AndroidFxAccount fxAccount;
protected final FxAccountNotificationManager notificationManager;
public SyncDelegate(Context context, CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount) {
public SyncDelegate(Context context, CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount,
FxAccountNotificationManager notificationManager) {
if (context == null) {
throw new IllegalArgumentException("context must not be null");
}
@ -103,10 +99,14 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
if (fxAccount == null) {
throw new IllegalArgumentException("fxAccount must not be null");
}
if (notificationManager == null) {
throw new IllegalArgumentException("notificationManager must not be null");
}
this.context = context;
this.latch = latch;
this.syncResult = syncResult;
this.fxAccount = fxAccount;
this.notificationManager = notificationManager;
}
/**
@ -171,7 +171,6 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
*/
public void handleCannotSync(State finalState) {
Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
showNotification(context, finalState);
setSyncResultSoftError();
latch.countDown();
}
@ -395,34 +394,6 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate);
}
protected static void showNotification(Context context, State state) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final Action action = state.getNeededAction();
if (action == Action.None) {
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs no action; cancelling any existing notification.");
notificationManager.cancel(NOTIFICATION_ID);
return;
}
final String title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
final String text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
FxAccountConstants.pii(LOG_TAG, "And text: " + text);
final Intent notificationIntent = new Intent(context, FxAccountStatusActivity.class);
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
final Builder builder = new NotificationCompat.Builder(context);
builder
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(R.drawable.ic_status_logo)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
notificationManager.notify(NOTIFICATION_ID, builder.build());
}
/**
* A trivial Sync implementation that does not cache client keys,
* certificates, or tokens.
@ -454,7 +425,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
}
final CountDownLatch latch = new CountDownLatch(1);
final SyncDelegate syncDelegate = new SyncDelegate(context, latch, syncResult, fxAccount);
final SyncDelegate syncDelegate = new SyncDelegate(context, latch, syncResult, fxAccount, notificationManager);
try {
final State state;
@ -524,6 +495,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
fxAccount.setState(state);
schedulePolicy.onHandleFinal(state.getNeededAction());
notificationManager.update(context, fxAccount);
try {
if (state.getStateLabel() != StateLabel.Married) {
syncDelegate.handleCannotSync(state);

View File

@ -174,6 +174,7 @@
<!ENTITY fxaccount_update_credentials_unknown_error 'Could not sign in'>
<!ENTITY fxaccount_status_header 'Firefox account'>
<!ENTITY fxaccount_status_signed_in_as 'Signed in as'>
<!ENTITY fxaccount_status_sync '&syncBrand.shortName.label;'>
<!ENTITY fxaccount_status_sync_enabled '&syncBrand.shortName.label;: enabled'>
<!ENTITY fxaccount_status_needs_verification2 'Your account needs to be verified. Tap to resend verification email.'>

View File

@ -154,6 +154,7 @@ gbjar.sources += [
'GeckoMessageReceiver.java',
'GeckoNetworkManager.java',
'GeckoProfile.java',
'GeckoProfileDirectories.java',
'GeckoScreenOrientation.java',
'GeckoSmsManager.java',
'GeckoThread.java',

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2010, 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.
*/
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:background="@android:color/transparent">
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="0px"
android:layout_weight="1"
android:paddingTop="0dip"
android:paddingBottom="@dimen/preference_fragment_padding_bottom"
android:paddingLeft="@dimen/preference_fragment_padding_side"
android:paddingRight="@dimen/preference_fragment_padding_side"
android:scrollbarStyle="@integer/preference_fragment_scrollbarStyle"
android:clipToPadding="false"
android:drawSelectorOnTop="false"
android:cacheColorHint="@android:color/transparent"
android:scrollbarAlwaysDrawVerticalTrack="true" />
</LinearLayout>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@color/fxaccount_error_preference_backgroundcolor"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingRight="?android:attr/scrollbarSize" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minWidth="0dp"
android:orientation="horizontal" >
<ImageView
android:id="@+android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="48dip"
android:padding="10dip" />
</LinearLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="6dip"
android:layout_marginLeft="15dip"
android:layout_marginRight="6dip"
android:layout_marginTop="6dip"
android:layout_weight="1" >
<TextView
android:id="@+android:id/title"
style="@style/FxAccountTextItem"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical" >
</TextView>
</RelativeLayout>
<!-- We ignore summary and widget_frame, but they still need to be present. We set them to be gone. -->
<TextView
android:id="@+android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="4"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone" />
<!-- Preference should place its actual preference widget here. -->
<LinearLayout
android:id="@+android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone" />
</LinearLayout>

View File

@ -38,7 +38,7 @@
<item android:id="@+id/launcher_add"
android:title="@string/contextmenu_add_to_launcher" />
<item android:id="@+id/subscribe"
<item android:id="@+id/save_subscribe"
android:title="@string/contextmenu_subscribe" />
</menu>

View File

@ -38,7 +38,7 @@
<item android:id="@+id/launcher_add"
android:title="@string/contextmenu_add_to_launcher" />
<item android:id="@+id/subscribe"
<item android:id="@+id/save_subscribe"
android:title="@string/contextmenu_subscribe" />
</menu>

View File

@ -38,7 +38,7 @@
<item android:id="@+id/launcher_add"
android:title="@string/contextmenu_add_to_launcher" />
<item android:id="@+id/subscribe"
<item android:id="@+id/save_subscribe"
android:title="@string/contextmenu_subscribe" />
</menu>

View File

@ -35,7 +35,7 @@
<item android:id="@+id/launcher_add"
android:title="@string/contextmenu_add_to_launcher" />
<item android:id="@+id/subscribe"
<item android:id="@+id/save_subscribe"
android:title="@string/contextmenu_subscribe" />
</menu>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- FxAccountStatusActivity ActionBar -->
<style name="ActionBar.FxAccountStatusActivity">
<item name="android:displayOptions">showHome|homeAsUp|showTitle</item>
</style>
<style name="FxAccountTheme" parent="@style/Gecko" />
<style name="FxAccountTheme.FxAccountStatusActivity" parent="@FxAccountTheme">
<item name="android:windowActionBar">true</item>
<item name="android:windowNoTitle">false</item>
<item name="android:actionBarStyle">@style/ActionBar.FxAccountStatusActivity</item>
</style>
</resources>

View File

@ -12,6 +12,9 @@
<color name="fxaccount_linkified_textColorLinkSubdued">#c0c9d0</color>
<color name="fxaccount_error">#d63920</color>
<color name="fxaccount_error_preference_backgroundcolor">#fad4d2</color>
<color name="fxaccount_button_textColor">#ffffff</color>
<color name="fxaccount_button_background_active">#e66000</color>
<color name="fxaccount_button_background_hit">#fd9500</color>

View File

@ -7,4 +7,11 @@
<dimen name="fxaccount_stroke_width">1dp</dimen>
<dimen name="fxaccount_corner_radius">5dp</dimen>
<dimen name="fxaccount_input_padding">10dp</dimen>
<!-- Preference fragment padding, bottom -->
<dimen name="preference_fragment_padding_bottom">0dp</dimen>
<!-- Preference fragment padding, sides -->
<dimen name="preference_fragment_padding_side">16dp</dimen>
<integer name="preference_fragment_scrollbarStyle">0x02000000</integer> <!-- outsideOverlay -->
</resources>

View File

@ -9,6 +9,10 @@
<style name="FxAccountTheme" parent="@style/Gecko" />
<style name="FxAccountTheme.FxAccountStatusActivity" parent="@style/FxAccountTheme">
<item name="android:windowNoTitle">false</item>
</style>
<style name="FxAccountMiddle">
<item name="android:background">@android:color/white</item>
<item name="android:orientation">vertical</item>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:key="status_screen" >
<PreferenceCategory
android:key="signed_in_as_category"
android:title="@string/fxaccount_status_signed_in_as" >
<Preference
android:editable="false"
android:key="email"
android:persistent="false"
android:title="@string/fxaccount_email_hint" />
</PreferenceCategory>
<PreferenceCategory
android:key="sync_category"
android:title="@string/fxaccount_status_sync" >
<Preference
android:editable="false"
android:icon="@drawable/fxaccount_sync_error"
android:key="needs_credentials"
android:layout="@layout/fxaccount_status_error_preference"
android:persistent="false"
android:title="@string/fxaccount_status_needs_credentials" />
<Preference
android:editable="false"
android:icon="@drawable/fxaccount_sync_error"
android:key="needs_upgrade"
android:layout="@layout/fxaccount_status_error_preference"
android:persistent="false"
android:title="@string/fxaccount_status_needs_upgrade" />
<Preference
android:editable="false"
android:icon="@drawable/fxaccount_sync_error"
android:key="needs_verification"
android:layout="@layout/fxaccount_status_error_preference"
android:persistent="false"
android:title="@string/fxaccount_status_needs_verification" />
<CheckBoxPreference
android:key="bookmarks"
android:persistent="false"
android:title="@string/fxaccount_status_bookmarks" />
<CheckBoxPreference
android:key="history"
android:persistent="false"
android:title="@string/fxaccount_status_history" />
<CheckBoxPreference
android:key="tabs"
android:persistent="false"
android:title="@string/fxaccount_status_tabs" />
<CheckBoxPreference
android:key="passwords"
android:persistent="false"
android:title="@string/fxaccount_status_passwords" />
</PreferenceCategory>
<PreferenceCategory
android:key="legal_category"
android:title="@string/fxaccount_status_legal" >
<Preference android:title="@string/fxaccount_policy_linktos" >
<intent
android:action="android.intent.action.VIEW"
android:data="@string/fxaccount_link_tos"
android:targetClass="@string/browser_intent_class"
android:targetPackage="@string/browser_intent_package" />
</Preference>
<Preference android:title="@string/fxaccount_policy_linkprivacy" >
<intent
android:action="android.intent.action.VIEW"
android:data="@string/fxaccount_link_pn"
android:targetClass="@string/browser_intent_class"
android:targetPackage="@string/browser_intent_package" />
</Preference>
</PreferenceCategory>
<PreferenceCategory
android:key="debug_category" >
<Preference android:key="debug_refresh" />
<Preference android:key="debug_dump" />
<Preference android:key="debug_force_sync" />
<Preference android:key="debug_forget_certificate" />
<Preference android:key="debug_require_password" />
<Preference android:key="debug_require_upgrade" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -99,7 +99,7 @@ skip-if = processor == "x86"
#[testBrowserProviderPerf]
# Using UITest
#[testAboutHomePageNavigation] # see bug 947550 and bug 977952
#[testAboutHomePageNavigation] # see bug 947550, bug 979038 and bug 977952
[testAboutHomeVisibility]
[testInputConnection]
[testSessionHistory]

View File

@ -1,7 +1,6 @@
<activity
android:theme="@style/FxAccountTheme"
android:icon="@drawable/fxaccount_icon"
android:label="Status"
android:theme="@style/FxAccountTheme.FxAccountStatusActivity"
android:label="@string/fxaccount_status_activity_label"
android:clearTaskOnLaunch="true"
android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA"
android:name="org.mozilla.gecko.fxa.activities.FxAccountStatusActivity"
@ -16,8 +15,7 @@
<activity
android:theme="@style/FxAccountTheme"
android:icon="@drawable/fxaccount_icon"
android:label="Setup"
android:label="@string/sync_app_name"
android:clearTaskOnLaunch="true"
android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA"
android:name="org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity"
@ -32,42 +30,36 @@
<activity
android:theme="@style/FxAccountTheme"
android:icon="@drawable/fxaccount_icon"
android:name="org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:icon="@drawable/fxaccount_icon"
android:name="org.mozilla.gecko.fxa.activities.FxAccountConfirmAccountActivity"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:icon="@drawable/fxaccount_icon"
android:name="org.mozilla.gecko.fxa.activities.FxAccountSignInActivity"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:icon="@drawable/fxaccount_icon"
android:name="org.mozilla.gecko.fxa.activities.FxAccountVerifiedAccountActivity"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:icon="@drawable/fxaccount_icon"
android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivity"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:icon="@drawable/fxaccount_icon"
android:name="org.mozilla.gecko.fxa.activities.FxAccountCreateAccountNotAllowedActivity"
android:windowSoftInputMode="adjustResize">
</activity>

View File

@ -98,6 +98,9 @@
<string name="sync_text_tab_sent">&sync.text.tab.sent.label;</string>
<string name="sync_text_tab_not_sent">&sync.text.tab.not.sent.label;</string>
<string name="browser_intent_package">@ANDROID_PACKAGE_NAME@</string>
<string name="browser_intent_class">@ANDROID_PACKAGE_NAME@.App</string>
<!-- Firefox account strings. -->
<!-- Firefox account links. -->
@ -162,7 +165,9 @@
<string name="fxaccount_update_credentials_button_label">&fxaccount_update_credentials_button_label;</string>
<string name="fxaccount_update_credentials_unknown_error">&fxaccount_update_credentials_unknown_error;</string>
<string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string>
<string name="fxaccount_status_header">&fxaccount_status_header;</string>
<string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string>
<string name="fxaccount_status_sync">&fxaccount_status_sync;</string>
<string name="fxaccount_status_sync_enabled">&fxaccount_status_sync_enabled;</string>
<string name="fxaccount_status_needs_verification">&fxaccount_status_needs_verification2;</string>

View File

@ -7,7 +7,7 @@
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8"
android:targetSdkVersion="16" />
android:targetSdkVersion="14" />
<uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
<uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/>

View File

@ -99,7 +99,7 @@ ScrollFrameTo(nsIScrollableFrame* aFrame, const CSSPoint& aPoint, bool& aSuccess
aSuccessOut = false;
if (!aFrame) {
return CSSPoint();
return aPoint;
}
// If the scrollable frame is currently in the middle of an async or smooth