--HG--
extra : amend_source : 08a7eb6769513fc676b7d09f337f4e57a09978d7
This commit is contained in:
Tim Taubert 2014-06-23 14:34:34 +02:00
Родитель 2d11183756 00a37a577b
Коммит 502722d134
104 изменённых файлов: 1961 добавлений и 1283 удалений

Просмотреть файл

@ -188,5 +188,11 @@ var gContentPane = {
document.documentElement.openWindow("Browser:TranslationExceptions", document.documentElement.openWindow("Browser:TranslationExceptions",
"chrome://browser/content/preferences/translation.xul", "chrome://browser/content/preferences/translation.xul",
"", null); "", null);
},
openTranslationProviderAttribution: function ()
{
Components.utils.import("resource:///modules/translation/Translation.jsm");
Translation.openProviderAttribution();
} }
}; };

Просмотреть файл

@ -144,10 +144,16 @@
oncommand="gContentPane.showLanguages();"/> oncommand="gContentPane.showLanguages();"/>
</row> </row>
<row id="translationBox" hidden="true"> <row id="translationBox" hidden="true">
<checkbox id="translate" preference="browser.translation.detectLanguage" flex="1" <hbox align="center">
label="&translateWebPages.label;" accesskey="&translateWebPages.accesskey;" <checkbox id="translate" preference="browser.translation.detectLanguage"
label="&translateWebPages.label;." accesskey="&translateWebPages.accesskey;"
onsyncfrompreference="return gContentPane.updateButtons('translateButton', onsyncfrompreference="return gContentPane.updateButtons('translateButton',
'browser.translation.detectLanguage');"/> 'browser.translation.detectLanguage');"/>
<label>Übersetzungen von</label>
<image id="translationAttributionImage" aria-label="Microsoft Translator"
onclick="gContentPane.openTranslationProviderAttribution()"
src="chrome://browser/content/microsoft-translator-attribution.png"/>
</hbox>
<button id="translateButton" label="&translateExceptions.label;" <button id="translateButton" label="&translateExceptions.label;"
oncommand="gContentPane.showTranslationExceptions();" oncommand="gContentPane.showTranslationExceptions();"
accesskey="&translateExceptions.accesskey;"/> accesskey="&translateExceptions.accesskey;"/>

Просмотреть файл

@ -186,5 +186,11 @@ var gContentPane = {
{ {
openDialog("chrome://browser/content/preferences/translation.xul", openDialog("chrome://browser/content/preferences/translation.xul",
"Browser:TranslationExceptions", null); "Browser:TranslationExceptions", null);
},
openTranslationProviderAttribution: function ()
{
Components.utils.import("resource:///modules/translation/Translation.jsm");
Translation.openProviderAttribution();
} }
}; };

Просмотреть файл

@ -134,10 +134,17 @@
</hbox> </hbox>
<hbox id="translationBox" hidden="true"> <hbox id="translationBox" hidden="true">
<checkbox id="translate" preference="browser.translation.detectLanguage" flex="1" <hbox align="center" flex="1">
label="&translateWebPages.label;" accesskey="&translateWebPages.accesskey;" <checkbox id="translate" preference="browser.translation.detectLanguage"
label="&translateWebPages.label;." accesskey="&translateWebPages.accesskey;"
onsyncfrompreference="return gContentPane.updateButtons('translateButton', onsyncfrompreference="return gContentPane.updateButtons('translateButton',
'browser.translation.detectLanguage');"/> 'browser.translation.detectLanguage');"/>
<label>Übersetzungen von</label>
<separator orient="vertical" class="thin"/>
<image id="translationAttributionImage" aria-label="Microsoft Translator"
onclick="gContentPane.openTranslationProviderAttribution()"
src="chrome://browser/content/microsoft-translator-attribution.png"/>
</hbox>
<button id="translateButton" label="&translateExceptions.label;" <button id="translateButton" label="&translateExceptions.label;"
oncommand="gContentPane.showTranslationExceptions();" oncommand="gContentPane.showTranslationExceptions();"
accesskey="&translateExceptions.accesskey;"/> accesskey="&translateExceptions.accesskey;"/>

Просмотреть файл

@ -80,6 +80,12 @@ this.Translation = {
if (trUI.shouldShowInfoBar(aBrowser.currentURI)) if (trUI.shouldShowInfoBar(aBrowser.currentURI))
trUI.showTranslationInfoBar(); trUI.showTranslationInfoBar();
},
openProviderAttribution: function() {
Cu.import("resource:///modules/RecentWindow.jsm");
RecentWindow.getMostRecentBrowserWindow().openUILinkIn(
"http://aka.ms/MicrosoftTranslatorAttribution", "tab");
} }
}; };
@ -305,6 +311,13 @@ let TranslationHealthReport = {
this._withProvider(provider => provider.recordLanguageChange(beforeFirstTranslation)); this._withProvider(provider => provider.recordLanguageChange(beforeFirstTranslation));
}, },
/**
* Record a denied translation offer.
*/
recordDeniedTranslationOffer: function () {
this._withProvider(provider => provider.recordDeniedTranslationOffer());
},
/** /**
* Retrieve the translation provider and pass it to the given function. * Retrieve the translation provider and pass it to the given function.
* *
@ -363,6 +376,7 @@ TranslationMeasurement1.prototype = Object.freeze({
pageTranslatedCountsByLanguage: DAILY_LAST_TEXT_FIELD, pageTranslatedCountsByLanguage: DAILY_LAST_TEXT_FIELD,
detectedLanguageChangedBefore: DAILY_COUNTER_FIELD, detectedLanguageChangedBefore: DAILY_COUNTER_FIELD,
detectedLanguageChangedAfter: DAILY_COUNTER_FIELD, detectedLanguageChangedAfter: DAILY_COUNTER_FIELD,
deniedTranslationOffer: DAILY_COUNTER_FIELD,
detectLanguageEnabled: DAILY_LAST_NUMERIC_FIELD, detectLanguageEnabled: DAILY_LAST_NUMERIC_FIELD,
showTranslationUI: DAILY_LAST_NUMERIC_FIELD, showTranslationUI: DAILY_LAST_NUMERIC_FIELD,
}, },
@ -499,6 +513,15 @@ TranslationProvider.prototype = Object.freeze({
}.bind(this)); }.bind(this));
}, },
recordDeniedTranslationOffer: function () {
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
TranslationMeasurement1.prototype.version);
return this._enqueueTelemetryStorageTask(function* recordTask() {
yield m.incrementDailyCounter("deniedTranslationOffer");
}.bind(this));
},
collectDailyData: function () { collectDailyData: function () {
let m = this.getMeasurement(TranslationMeasurement1.prototype.name, let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
TranslationMeasurement1.prototype.version); TranslationMeasurement1.prototype.version);

Просмотреть файл

@ -3,3 +3,4 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar: browser.jar:
content/browser/translation-infobar.xml content/browser/translation-infobar.xml
content/browser/microsoft-translator-attribution.png

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

Просмотреть файл

@ -196,6 +196,28 @@ add_task(function* test_record_translation() {
yield storage.close(); yield storage.close();
}); });
add_task(function* test_denied_translation_offer() {
let storage = yield Metrics.Storage("translation");
let provider = new TranslationProvider();
yield provider.init(storage);
let now = new Date();
yield provider.recordDeniedTranslationOffer();
yield provider.recordDeniedTranslationOffer();
let m = provider.getMeasurement("translation", 1);
let values = yield m.getValues();
Assert.equal(values.days.size, 1);
Assert.ok(values.days.hasDay(now));
let day = values.days.getDay(now);
Assert.ok(day.has("deniedTranslationOffer"));
Assert.equal(day.get("deniedTranslationOffer"), 2);
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_collect_daily() { add_task(function* test_collect_daily() {
let storage = yield Metrics.Storage("translation"); let storage = yield Metrics.Storage("translation");
let provider = new TranslationProvider(); let provider = new TranslationProvider();
@ -253,6 +275,8 @@ add_task(function* test_healthreporter_json() {
yield provider.recordTranslationOpportunity("es", now); yield provider.recordTranslationOpportunity("es", now);
yield provider.recordTranslation("es", "en", 1000, now); yield provider.recordTranslation("es", "en", 1000, now);
yield provider.recordDeniedTranslationOffer();
yield reporter.collectMeasurements(); yield reporter.collectMeasurements();
let payload = yield reporter.getJSONPayload(true); let payload = yield reporter.getJSONPayload(true);
let today = reporter._formatDate(now); let today = reporter._formatDate(now);
@ -285,6 +309,9 @@ add_task(function* test_healthreporter_json() {
Assert.equal(translations["detectedLanguageChangedBefore"], 1); Assert.equal(translations["detectedLanguageChangedBefore"], 1);
Assert.ok("detectedLanguageChangedAfter" in translations); Assert.ok("detectedLanguageChangedAfter" in translations);
Assert.equal(translations["detectedLanguageChangedAfter"], 1); Assert.equal(translations["detectedLanguageChangedAfter"], 1);
Assert.ok("deniedTranslationOffer" in translations);
Assert.equal(translations["deniedTranslationOffer"], 1);
} finally { } finally {
reporter._shutdown(); reporter._shutdown();
} }
@ -309,6 +336,8 @@ add_task(function* test_healthreporter_json2() {
yield provider.recordTranslationOpportunity("es", now); yield provider.recordTranslationOpportunity("es", now);
yield provider.recordTranslation("es", "en", 1000, now); yield provider.recordTranslation("es", "en", 1000, now);
yield provider.recordDeniedTranslationOffer();
yield reporter.collectMeasurements(); yield reporter.collectMeasurements();
let payload = yield reporter.getJSONPayload(true); let payload = yield reporter.getJSONPayload(true);
let today = reporter._formatDate(now); let today = reporter._formatDate(now);
@ -327,6 +356,7 @@ add_task(function* test_healthreporter_json2() {
Assert.ok(!("pageTranslatedCountsByLanguage" in translations)); Assert.ok(!("pageTranslatedCountsByLanguage" in translations));
Assert.ok(!("detectedLanguageChangedBefore" in translations)); Assert.ok(!("detectedLanguageChangedBefore" in translations));
Assert.ok(!("detectedLanguageChangedAfter" in translations)); Assert.ok(!("detectedLanguageChangedAfter" in translations));
Assert.ok(!("deniedTranslationOffer" in translations));
} finally { } finally {
reporter._shutdown(); reporter._shutdown();
} }

Просмотреть файл

@ -98,7 +98,9 @@
class="translate-infobar-element options-menu-button" class="translate-infobar-element options-menu-button"
anonid="options" anonid="options"
label="&translation.options.menu;"> label="&translation.options.menu;">
<xul:menupopup onpopupshowing="document.getBindingParent(this).optionsShowing();"> <xul:menupopup class="translation-menupopup cui-widget-panel cui-widget-panelview
cui-widget-panelWithFooter PanelUI-subView"
onpopupshowing="document.getBindingParent(this).optionsShowing();">
<xul:menuitem anonid="neverForLanguage" <xul:menuitem anonid="neverForLanguage"
oncommand="document.getBindingParent(this).neverForLanguage();"/> oncommand="document.getBindingParent(this).neverForLanguage();"/>
<xul:menuitem anonid="neverForSite" <xul:menuitem anonid="neverForSite"
@ -109,6 +111,12 @@
<xul:menuitem oncommand="openPreferences('paneContent');" <xul:menuitem oncommand="openPreferences('paneContent');"
label="&translation.options.preferences.label;" label="&translation.options.preferences.label;"
accesskey="&translation.options.preferences.accesskey;"/> accesskey="&translation.options.preferences.accesskey;"/>
<xul:menuitem class="translation-attribution subviewbutton panel-subview-footer"
oncommand="document.getBindingParent(this).openProviderAttribution();">
<xul:label>Übersetzungen von</xul:label>
<xul:image src="chrome://browser/content/microsoft-translator-attribution.png"
aria-label="Microsoft Translator"/>
</xul:menuitem>
</xul:menupopup> </xul:menupopup>
</xul:button> </xul:button>
@ -310,6 +318,14 @@
</body> </body>
</method> </method>
<method name="openProviderAttribution">
<body>
<![CDATA[
Translation.openProviderAttribution();
]]>
</body>
</method>
</implementation> </implementation>
</binding> </binding>
</bindings> </bindings>

Просмотреть файл

@ -54,6 +54,12 @@ label.small {
margin: 5px; margin: 5px;
} }
/* Content Pane */
#translationAttributionImage {
width: 70px;
cursor: pointer;
}
/* Applications Pane */ /* Applications Pane */
#BrowserPreferences[animated="true"] #handlersView { #BrowserPreferences[animated="true"] #handlersView {
height: 25em; height: 25em;

Просмотреть файл

@ -140,6 +140,11 @@ caption {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
} }
#translationAttributionImage {
width: 70px;
cursor: pointer;
}
#browserUseCurrent, #browserUseCurrent,
#browserUseBookmark, #browserUseBookmark,
#browserUseBlank { #browserUseBlank {

Просмотреть файл

@ -46,3 +46,21 @@ notification[value="translation"] menulist > .menulist-dropmarker {
margin: auto; margin: auto;
padding: 5px 0; padding: 5px 0;
} }
.translation-menupopup arrowscrollbox {
padding-bottom: 0;
}
.translation-attribution {
cursor: pointer;
-moz-box-align: end;
font-size: small;
}
.translation-attribution > label {
margin-bottom: 0;
}
.translation-attribution > image {
width: 70px;
}

Просмотреть файл

@ -2443,6 +2443,10 @@ notification[value="translation"] {
-moz-image-region: rect(0px, 32px, 16px, 16px); -moz-image-region: rect(0px, 32px, 16px, 16px);
} }
.translation-menupopup {
-moz-appearance: none;
}
/* Bookmarks roots menu-items */ /* Bookmarks roots menu-items */
#subscribeToPageMenuitem:not([disabled]), #subscribeToPageMenuitem:not([disabled]),
#subscribeToPageMenupopup, #subscribeToPageMenupopup,

Просмотреть файл

@ -53,6 +53,12 @@ label.small {
margin: 6px; margin: 6px;
} }
/* Content Pane */
#translationAttributionImage {
width: 70px;
cursor: pointer;
}
/* Applications Pane */ /* Applications Pane */
#BrowserPreferences[animated="true"] #handlersView { #BrowserPreferences[animated="true"] #handlersView {
height: 25em; height: 25em;

Просмотреть файл

@ -38,6 +38,7 @@ SEARCH_PATHS = [
'config', 'config',
'dom/bindings', 'dom/bindings',
'dom/bindings/parser', 'dom/bindings/parser',
'layout/tools/reftest',
'other-licenses/ply', 'other-licenses/ply',
'xpcom/idl-parser', 'xpcom/idl-parser',
'testing', 'testing',

Просмотреть файл

@ -17,6 +17,7 @@ mozilla.pth:config
mozilla.pth:xpcom/typelib/xpt/tools mozilla.pth:xpcom/typelib/xpt/tools
mozilla.pth:dom/bindings mozilla.pth:dom/bindings
mozilla.pth:dom/bindings/parser mozilla.pth:dom/bindings/parser
mozilla.pth:layout/tools/reftest
moztreedocs.pth:tools/docs moztreedocs.pth:tools/docs
copy:build/buildconfig.py copy:build/buildconfig.py
packages.txt:testing/mozbase/packages.txt packages.txt:testing/mozbase/packages.txt

Просмотреть файл

@ -34,3 +34,6 @@ MOCHITEST_MANIFESTS += [
'reftests/fonts/mochitest.ini', 'reftests/fonts/mochitest.ini',
'reftests/fonts/mplus/mochitest.ini', 'reftests/fonts/mplus/mochitest.ini',
] ]
REFTEST_MANIFESTS += ['reftests/reftest.list']
CRASHTEST_MANIFESTS += ['../testing/crashtest/crashtests.list']

Просмотреть файл

@ -3,74 +3,24 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
import sys, os.path, re import os
import sys
commentRE = re.compile(r"\s+#") from reftest import ReftestManifest
conditionsRE = re.compile(r"^(fails|needs-focus|random|skip|asserts|slow|require-or|silentfail|pref|test-pref|ref-pref|fuzzy)")
httpRE = re.compile(r"HTTP\((\.\.(\/\.\.)*)\)")
protocolRE = re.compile(r"^\w+:")
def parseManifest(manifest, dirs):
"""Parse the reftest manifest |manifest|, adding all directories containing
tests (and the dirs containing the manifests themselves) to the set |dirs|."""
manifestdir = os.path.dirname(os.path.abspath(manifest))
dirs.add(manifestdir)
f = file(manifest)
urlprefix = ''
for line in f:
if line[0] == '#':
continue # entire line was a comment
m = commentRE.search(line)
if m:
line = line[:m.start()]
line = line.strip()
if not line:
continue
items = line.split()
while conditionsRE.match(items[0]):
del items[0]
if items[0] == "HTTP":
del items[0]
m = httpRE.match(items[0])
if m:
# need to package the dir referenced here
d = os.path.normpath(os.path.join(manifestdir, m.group(1)))
dirs.add(d)
del items[0]
if items[0] == "url-prefix":
urlprefix = items[1]
continue
elif items[0] == "default-preferences":
continue
elif items[0] == "include":
parseManifest(os.path.join(manifestdir, items[1]), dirs)
continue
elif items[0] == "load" or items[0] == "script":
testURLs = [items[1]]
elif items[0] == "==" or items[0] == "!=":
testURLs = items[1:3]
for u in testURLs:
m = protocolRE.match(u)
if m:
# can't very well package about: or data: URIs
continue
d = os.path.dirname(os.path.normpath(os.path.join(manifestdir, urlprefix + u)))
dirs.add(d)
f.close()
def printTestDirs(topsrcdir, topmanifests): def printTestDirs(topsrcdir, topmanifests):
"""Parse |topmanifests| and print a list of directories containing the tests """Parse |topmanifests| and print a list of directories containing the tests
within (and the manifests including those tests), relative to |topsrcdir|.""" within (and the manifests including those tests), relative to |topsrcdir|.
"""
topsrcdir = os.path.abspath(topsrcdir) topsrcdir = os.path.abspath(topsrcdir)
dirs = set() dirs = set()
for manifest in topmanifests: for path in topmanifests:
parseManifest(manifest, dirs) m = ReftestManifest()
for dir in sorted(dirs): m.load(path)
d = dir[len(topsrcdir):].replace('\\','/') dirs |= m.dirs
if d[0] == '/':
d = d[1:] for d in sorted(dirs):
print d d = d[len(topsrcdir):].replace('\\', '/').lstrip('/')
print(d)
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) < 3: if len(sys.argv) < 3:

Просмотреть файл

@ -0,0 +1,125 @@
# 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/.
from __future__ import unicode_literals
import os
import re
RE_COMMENT = re.compile(r'\s+#')
RE_HTTP = re.compile(r'HTTP\((\.\.(\/\.\.)*)\)')
RE_PROTOCOL = re.compile(r'^\w+:')
FAILURE_TYPES = (
'fails',
'fails-if',
'needs-focus',
'random',
'random-if',
'silentfail',
'silentfail-if',
'skip',
'skip-if',
'slow',
'slow-if',
'fuzzy',
'fuzzy-if',
'require-or',
'asserts',
'asserts-if',
)
PREF_ITEMS = (
'pref',
'test-pref',
'ref-pref',
)
class ReftestManifest(object):
"""Represents a parsed reftest manifest.
We currently only capture file information because that is the only thing
tools require.
"""
def __init__(self):
self.path = None
self.dirs = set()
self.files = set()
self.manifests = set()
def load(self, path):
"""Parse a reftest manifest file."""
normalized = os.path.normpath(os.path.abspath(path))
self.manifests.add(normalized)
if not self.path:
self.path = normalized
mdir = os.path.dirname(normalized)
self.dirs.add(mdir)
with open(path, 'r') as fh:
urlprefix = ''
for line in fh:
line = line.decode('utf-8')
# Entire line is a comment.
if line.startswith('#'):
continue
# Comments can begin mid line. Strip them.
m = RE_COMMENT.search(line)
if m:
line = line[:m.start()]
line = line.strip()
if not line:
continue
items = line.split()
tests = []
for i in range(len(items)):
item = items[i]
if item.startswith(FAILURE_TYPES):
continue
if item.startswith(PREF_ITEMS):
continue
if item == 'HTTP':
continue
m = RE_HTTP.match(item)
if m:
# Need to package the referenced directory.
self.dirs.add(os.path.normpath(os.path.join(
mdir, m.group(1))))
continue
if item == 'url-prefix':
urlprefix = items[i+1]
break
if item == 'default-preferences':
break
if item == 'include':
self.load(os.path.join(mdir, items[i+1]))
break
if item == 'load' or item == 'script':
tests.append(items[i+1])
break
if item == '==' or item == '!=':
tests.extend(items[i+1:i+3])
break
for f in tests:
# We can't package about: or data: URIs.
# Discarding data isn't correct for a parser. But retaining
# all data isn't currently a requirement.
if RE_PROTOCOL.match(f):
continue
test = os.path.normpath(os.path.join(mdir, urlprefix + f))
self.files.add(test)
self.dirs.add(os.path.dirname(test))

Просмотреть файл

@ -121,7 +121,7 @@ pref("browser.sessionhistory.max_entries", 50);
pref("browser.sessionstore.resume_session_once", false); pref("browser.sessionstore.resume_session_once", false);
pref("browser.sessionstore.resume_from_crash", true); pref("browser.sessionstore.resume_from_crash", true);
pref("browser.sessionstore.interval", 10000); // milliseconds pref("browser.sessionstore.interval", 10000); // milliseconds
pref("browser.sessionstore.max_tabs_undo", 1); pref("browser.sessionstore.max_tabs_undo", 5);
pref("browser.sessionstore.max_resumed_crashes", 1); pref("browser.sessionstore.max_resumed_crashes", 1);
pref("browser.sessionstore.recent_crashes", 0); pref("browser.sessionstore.recent_crashes", 0);

Просмотреть файл

@ -1130,6 +1130,10 @@ abstract public class BrowserApp extends GeckoApp
// Do exactly the same thing as if you tapped 'Sync' in Settings. // Do exactly the same thing as if you tapped 'Sync' in Settings.
final Intent intent = new Intent(getContext(), FxAccountGetStartedActivity.class); final Intent intent = new Intent(getContext(), FxAccountGetStartedActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final NativeJSObject extras = message.optObject("extras", null);
if (extras != null) {
intent.putExtra("extras", extras.toString());
}
getContext().startActivity(intent); getContext().startActivity(intent);
} else if ("CharEncoding:Data".equals(event)) { } else if ("CharEncoding:Data".equals(event)) {

Просмотреть файл

@ -15,6 +15,7 @@ public class FxAccountConstants {
public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1"; public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1";
public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5"; public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5";
public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://api-accounts.stage.mozaws.net/v1";
public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://token.stage.mozaws.net/1.0/sync/1.5"; public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://token.stage.mozaws.net/1.0/sync/1.5";
// For extra debugging. Not final so it can be changed from Fennec, or from // For extra debugging. Not final so it can be changed from Fennec, or from

Просмотреть файл

@ -23,6 +23,7 @@ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged; import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State; import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.ProgressDisplay; import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.ProgressDisplay;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.setup.Constants; import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils; import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
@ -32,6 +33,7 @@ import android.accounts.AccountManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod; import android.text.method.PasswordTransformationMethod;
@ -50,6 +52,16 @@ import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay { abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay {
public static final String EXTRA_EMAIL = "email";
public static final String EXTRA_PASSWORD = "password";
public static final String EXTRA_PASSWORD_SHOWN = "password_shown";
public static final String EXTRA_YEAR = "year";
public static final String EXTRA_EXTRAS = "extras";
public static final String JSON_KEY_AUTH = "auth";
public static final String JSON_KEY_SERVICES = "services";
public static final String JSON_KEY_SYNC = "sync";
public FxAccountAbstractSetupActivity() { public FxAccountAbstractSetupActivity() {
super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT); super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT);
} }
@ -60,6 +72,10 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName(); private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName();
// By default, any custom server configuration is only shown when the account
// is configured to use a custom server.
private static boolean ALWAYS_SHOW_CUSTOM_SERVER_LAYOUT = false;
protected int minimumPasswordLength = 8; protected int minimumPasswordLength = 8;
protected AutoCompleteTextView emailEdit; protected AutoCompleteTextView emailEdit;
@ -69,18 +85,34 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
protected Button button; protected Button button;
protected ProgressBar progressBar; protected ProgressBar progressBar;
private String authServerEndpoint;
private String syncServerEndpoint;
protected String getAuthServerEndpoint() {
return authServerEndpoint;
}
protected String getTokenServerEndpoint() {
return syncServerEndpoint;
}
protected void createShowPasswordButton() { protected void createShowPasswordButton() {
showPasswordButton.setOnClickListener(new OnClickListener() { showPasswordButton.setOnClickListener(new OnClickListener() {
@SuppressWarnings("deprecation")
@Override @Override
public void onClick(View v) { public void onClick(View v) {
boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod; boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod;
setPasswordButtonShown(!isShown);
}
});
}
@SuppressWarnings("deprecation")
protected void setPasswordButtonShown(boolean shouldShow) {
// Changing input type loses position in edit text; let's try to maintain it. // Changing input type loses position in edit text; let's try to maintain it.
int start = passwordEdit.getSelectionStart(); int start = passwordEdit.getSelectionStart();
int stop = passwordEdit.getSelectionEnd(); int stop = passwordEdit.getSelectionEnd();
if (isShown) { if (!shouldShow) {
passwordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance()); passwordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance());
showPasswordButton.setText(R.string.fxaccount_password_show); showPasswordButton.setText(R.string.fxaccount_password_show);
showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background)); showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background));
@ -93,8 +125,6 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
} }
passwordEdit.setSelection(start, stop); passwordEdit.setSelection(start, stop);
} }
});
}
protected void linkifyPolicy() { protected void linkifyPolicy() {
TextView policyView = (TextView) ensureFindViewById(null, R.id.policy, "policy links"); TextView policyView = (TextView) ensureFindViewById(null, R.id.policy, "policy links");
@ -262,7 +292,7 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
AndroidFxAccount fxAccount; AndroidFxAccount fxAccount;
try { try {
final String profile = Constants.DEFAULT_PROFILE; final String profile = Constants.DEFAULT_PROFILE;
final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT; final String tokenServerURI = getTokenServerEndpoint();
// It is crucial that we use the email address provided by the server // It is crucial that we use the email address provided by the server
// (rather than whatever the user entered), because the user's keys are // (rather than whatever the user entered), because the user's keys are
// wrapped and salted with the initial email they provided to // wrapped and salted with the initial email they provided to
@ -356,6 +386,31 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
emailEdit.setAdapter(adapter); emailEdit.setAdapter(adapter);
} }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
protected void updateFromIntentExtras() {
// Only set email/password in onCreate; we don't want to overwrite edited values onResume.
if (getIntent() != null && getIntent().getExtras() != null) {
Bundle bundle = getIntent().getExtras();
emailEdit.setText(bundle.getString(EXTRA_EMAIL));
passwordEdit.setText(bundle.getString(EXTRA_PASSWORD));
setPasswordButtonShown(bundle.getBoolean(EXTRA_PASSWORD_SHOWN, false));
}
// This sets defaults as well as extracting from extras, so it's not conditional.
updateServersFromIntentExtras(getIntent());
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
FxAccountConstants.pii(LOG_TAG, "Using auth server: " + authServerEndpoint);
FxAccountConstants.pii(LOG_TAG, "Using sync server: " + syncServerEndpoint);
}
updateCustomServerView();
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
@ -370,4 +425,110 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
}; };
task.execute(); task.execute();
} }
protected Bundle makeExtrasBundle(String email, String password) {
final Bundle bundle = new Bundle();
// Pass through any extras that we were started with.
if (getIntent() != null && getIntent().getExtras() != null) {
bundle.putAll(getIntent().getExtras());
}
// Overwrite with current settings.
if (email == null) {
email = emailEdit.getText().toString();
}
if (password == null) {
password = passwordEdit.getText().toString();
}
bundle.putString(EXTRA_EMAIL, email);
bundle.putString(EXTRA_PASSWORD, password);
boolean isPasswordShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod;
bundle.putBoolean(EXTRA_PASSWORD_SHOWN, isPasswordShown);
return bundle;
}
protected void startActivityInstead(Class<?> cls, int requestCode, Bundle extras) {
Intent intent = new Intent(this, cls);
if (extras != null) {
intent.putExtras(extras);
}
// 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);
startActivityForResult(intent, requestCode);
}
protected void updateServersFromIntentExtras(Intent intent) {
// Start with defaults.
this.authServerEndpoint = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT;
this.syncServerEndpoint = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT;
if (intent == null) {
Logger.warn(LOG_TAG, "Intent is null; ignoring and using default servers.");
return;
}
final String extrasString = intent.getStringExtra(EXTRA_EXTRAS);
if (extrasString == null) {
return;
}
final ExtendedJSONObject extras;
final ExtendedJSONObject services;
try {
extras = new ExtendedJSONObject(extrasString);
services = extras.getObject(JSON_KEY_SERVICES);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception parsing extras; ignoring and using default servers.");
return;
}
String authServer = extras.getString(JSON_KEY_AUTH);
String syncServer = services == null ? null : services.getString(JSON_KEY_SYNC);
if (authServer != null) {
this.authServerEndpoint = authServer;
}
if (syncServer != null) {
this.syncServerEndpoint = syncServer;
}
if (FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServerEndpoint) &&
!FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServerEndpoint)) {
// We really don't want to hard-code assumptions about server
// configurations into client code in such a way that if and when the
// situation is relaxed, the client code stops valid usage. Instead, we
// warn. This configuration should present itself as an auth exception at
// Sync time.
Logger.warn(LOG_TAG, "Mozilla's Sync token servers only works with Mozilla's auth servers. Sync will likely be mis-configured.");
}
}
protected void updateCustomServerView() {
final boolean shouldShow =
ALWAYS_SHOW_CUSTOM_SERVER_LAYOUT ||
!FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServerEndpoint) ||
!FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServerEndpoint);
if (!shouldShow) {
setCustomServerViewVisibility(View.GONE);
return;
}
final TextView authServerView = (TextView) ensureFindViewById(null, R.id.account_server_summary, "account server");
final TextView syncServerView = (TextView) ensureFindViewById(null, R.id.sync_server_summary, "Sync server");
authServerView.setText(authServerEndpoint);
syncServerView.setText(syncServerEndpoint);
setCustomServerViewVisibility(View.VISIBLE);
}
protected void setCustomServerViewVisibility(int visibility) {
ensureFindViewById(null, R.id.account_server_layout, "account server layout").setVisibility(visibility);
ensureFindViewById(null, R.id.sync_server_layout, "sync server layout").setVisibility(visibility);
}
} }

Просмотреть файл

@ -89,32 +89,29 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
signInInsteadLink.setOnClickListener(new OnClickListener() { signInInsteadLink.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
final String email = emailEdit.getText().toString(); final Bundle extras = makeExtrasBundle(null, null);
final String password = passwordEdit.getText().toString(); startActivityInstead(FxAccountSignInActivity.class, CHILD_REQUEST_CODE, extras);
doSigninInstead(email, password);
} }
}); });
// Only set email/password in onCreate; we don't want to overwrite edited values onResume. updateFromIntentExtras();
if (getIntent() != null && getIntent().getExtras() != null) {
Bundle bundle = getIntent().getExtras();
emailEdit.setText(bundle.getString("email"));
passwordEdit.setText(bundle.getString("password"));
}
} }
protected void doSigninInstead(final String email, final String password) { @Override
Intent intent = new Intent(this, FxAccountSignInActivity.class); protected Bundle makeExtrasBundle(String email, String password) {
if (email != null) { final Bundle extras = super.makeExtrasBundle(email, password);
intent.putExtra("email", email); final String year = yearEdit.getText().toString();
extras.putString(EXTRA_YEAR, year);
return extras;
} }
if (password != null) {
intent.putExtra("password", password); @Override
protected void updateFromIntentExtras() {
super.updateFromIntentExtras();
if (getIntent() != null) {
yearEdit.setText(getIntent().getStringExtra(EXTRA_YEAR));
} }
// 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);
startActivityForResult(intent, CHILD_REQUEST_CODE);
} }
@Override @Override
@ -142,7 +139,9 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
email = emailEdit.getText().toString(); email = emailEdit.getText().toString();
} }
final String password = passwordEdit.getText().toString(); final String password = passwordEdit.getText().toString();
doSigninInstead(email, password);
final Bundle extras = makeExtrasBundle(email, password);
startActivityInstead(FxAccountSignInActivity.class, CHILD_REQUEST_CODE, extras);
} }
}, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); }, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance()); remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance());
@ -205,7 +204,7 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
} }
public void createAccount(String email, String password, Map<String, Boolean> engines) { public void createAccount(String email, String password, Map<String, Boolean> engines) {
String serverURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT; String serverURI = getAuthServerEndpoint();
PasswordStretcher passwordStretcher = makePasswordStretcher(password); PasswordStretcher passwordStretcher = makePasswordStretcher(password);
// This delegate creates a new Android account on success, opens the // This delegate creates a new Android account on success, opens the
// appropriate "success!" activity, and finishes this activity. // appropriate "success!" activity, and finishes this activity.

Просмотреть файл

@ -50,13 +50,24 @@ public class FxAccountGetStartedActivity extends AccountAuthenticatorActivity {
button.setOnClickListener(new OnClickListener() { button.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Intent intent = new Intent(FxAccountGetStartedActivity.this, FxAccountCreateAccountActivity.class); Bundle extras = null; // startFlow accepts null.
if (getIntent() != null) {
extras = getIntent().getExtras();
}
startFlow(extras);
}
});
}
protected void startFlow(Bundle extras) {
final Intent intent = new Intent(this, FxAccountCreateAccountActivity.class);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with // 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? // the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivityForResult(intent, CHILD_REQUEST_CODE); if (extras != null) {
intent.putExtras(extras);
} }
}); startActivityForResult(intent, CHILD_REQUEST_CODE);
} }
@Override @Override
@ -79,6 +90,20 @@ public class FxAccountGetStartedActivity extends AccountAuthenticatorActivity {
this.startActivity(intent); this.startActivity(intent);
this.finish(); this.finish();
} }
// If we've been launched with extras (namely custom server URLs), continue
// past go and collect 200 dollars. If we ever get back here (for example,
// if the user hits the back button), forget that we had extras entirely, so
// that we don't enter a loop.
Bundle extras = null;
if (getIntent() != null) {
extras = getIntent().getExtras();
}
if (extras != null && extras.containsKey(FxAccountAbstractSetupActivity.EXTRA_EXTRAS)) {
getIntent().replaceExtras(Bundle.EMPTY);
startFlow((Bundle) extras.clone());
return;
}
} }
/** /**

Просмотреть файл

@ -15,7 +15,6 @@ import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.PasswordStretcher; import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask; import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils; import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
@ -65,22 +64,12 @@ public class FxAccountSignInActivity extends FxAccountAbstractSetupActivity {
createAccountInsteadLink.setOnClickListener(new OnClickListener() { createAccountInsteadLink.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Intent intent = new Intent(FxAccountSignInActivity.this, FxAccountCreateAccountActivity.class); final Bundle extras = makeExtrasBundle(null, null);
intent.putExtra("email", emailEdit.getText().toString()); startActivityInstead(FxAccountCreateAccountActivity.class, CHILD_REQUEST_CODE, extras);
intent.putExtra("password", passwordEdit.getText().toString());
// 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);
startActivityForResult(intent, CHILD_REQUEST_CODE);
} }
}); });
// Only set email/password in onCreate; we don't want to overwrite edited values onResume. updateFromIntentExtras();
if (getIntent() != null && getIntent().getExtras() != null) {
Bundle bundle = getIntent().getExtras();
emailEdit.setText(bundle.getString("email"));
passwordEdit.setText(bundle.getString("password"));
}
TextView view = (TextView) findViewById(R.id.forgot_password_link); TextView view = (TextView) findViewById(R.id.forgot_password_link);
ActivityUtils.linkTextView(view, R.string.fxaccount_sign_in_forgot_password, R.string.fxaccount_link_forgot_password); ActivityUtils.linkTextView(view, R.string.fxaccount_sign_in_forgot_password, R.string.fxaccount_link_forgot_password);
@ -102,7 +91,7 @@ public class FxAccountSignInActivity extends FxAccountAbstractSetupActivity {
} }
public void signIn(String email, String password) { public void signIn(String email, String password) {
String serverURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT; String serverURI = getAuthServerEndpoint();
PasswordStretcher passwordStretcher = makePasswordStretcher(password); PasswordStretcher passwordStretcher = makePasswordStretcher(password);
// This delegate creates a new Android account on success, opens the // This delegate creates a new Android account on success, opens the
// appropriate "success!" activity, and finishes this activity. // appropriate "success!" activity, and finishes this activity.

Просмотреть файл

@ -18,6 +18,7 @@ import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State; import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender; import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.sync.SyncConfiguration;
@ -56,7 +57,17 @@ public class FxAccountStatusFragment
// collection. // collection.
private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000; private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
// By default, the auth/account server preference is only shown when the
// account is configured to use a custom server. In debug mode, this is set.
private static boolean ALWAYS_SHOW_AUTH_SERVER = false;
// By default, the Sync server preference is only shown when the account is
// configured to use a custom Sync server. In debug mode, this is set.
private static boolean ALWAYS_SHOW_SYNC_SERVER = false;
protected PreferenceCategory accountCategory;
protected Preference emailPreference; protected Preference emailPreference;
protected Preference authServerPreference;
protected Preference needsPasswordPreference; protected Preference needsPasswordPreference;
protected Preference needsUpgradePreference; protected Preference needsUpgradePreference;
@ -72,6 +83,7 @@ public class FxAccountStatusFragment
protected CheckBoxPreference passwordsPreference; protected CheckBoxPreference passwordsPreference;
protected EditTextPreference deviceNamePreference; protected EditTextPreference deviceNamePreference;
protected Preference syncServerPreference;
protected volatile AndroidFxAccount fxAccount; protected volatile AndroidFxAccount fxAccount;
// The contract is: when fxAccount is non-null, then clientsDataDelegate is // The contract is: when fxAccount is non-null, then clientsDataDelegate is
@ -105,7 +117,9 @@ public class FxAccountStatusFragment
protected void addPreferences() { protected void addPreferences() {
addPreferencesFromResource(R.xml.fxaccount_status_prefscreen); addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
accountCategory = (PreferenceCategory) ensureFindPreference("signed_in_as_category");
emailPreference = ensureFindPreference("email"); emailPreference = ensureFindPreference("email");
authServerPreference = ensureFindPreference("auth_server");
needsPasswordPreference = ensureFindPreference("needs_credentials"); needsPasswordPreference = ensureFindPreference("needs_credentials");
needsUpgradePreference = ensureFindPreference("needs_upgrade"); needsUpgradePreference = ensureFindPreference("needs_upgrade");
@ -124,6 +138,8 @@ public class FxAccountStatusFragment
removeDebugButtons(); removeDebugButtons();
} else { } else {
connectDebugButtons(); connectDebugButtons();
ALWAYS_SHOW_AUTH_SERVER = true;
ALWAYS_SHOW_SYNC_SERVER = true;
} }
needsPasswordPreference.setOnPreferenceClickListener(this); needsPasswordPreference.setOnPreferenceClickListener(this);
@ -137,6 +153,8 @@ public class FxAccountStatusFragment
deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name"); deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
deviceNamePreference.setOnPreferenceChangeListener(this); deviceNamePreference.setOnPreferenceChangeListener(this);
syncServerPreference = ensureFindPreference("sync_server");
} }
/** /**
@ -152,6 +170,10 @@ public class FxAccountStatusFragment
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
if (preference == needsPasswordPreference) { if (preference == needsPasswordPreference) {
Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class); Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
final Bundle extras = getExtrasForAccount();
if (extras != null) {
intent.putExtras(extras);
}
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with // 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? // the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
@ -190,6 +212,17 @@ public class FxAccountStatusFragment
return false; return false;
} }
protected Bundle getExtrasForAccount() {
final Bundle extras = new Bundle();
final ExtendedJSONObject o = new ExtendedJSONObject();
o.put(FxAccountAbstractSetupActivity.JSON_KEY_AUTH, fxAccount.getAccountServerURI());
final ExtendedJSONObject services = new ExtendedJSONObject();
services.put(FxAccountAbstractSetupActivity.JSON_KEY_SYNC, fxAccount.getTokenServerURI());
o.put(FxAccountAbstractSetupActivity.JSON_KEY_SERVICES, services);
extras.putString(FxAccountAbstractSetupActivity.EXTRA_EXTRAS, o.toJSONString());
return extras;
}
protected void setCheckboxesEnabled(boolean enabled) { protected void setCheckboxesEnabled(boolean enabled) {
bookmarksPreference.setEnabled(enabled); bookmarksPreference.setEnabled(enabled);
historyPreference.setEnabled(enabled); historyPreference.setEnabled(enabled);
@ -367,6 +400,8 @@ public class FxAccountStatusFragment
} }
emailPreference.setTitle(fxAccount.getEmail()); emailPreference.setTitle(fxAccount.getEmail());
updateAuthServerPreference();
updateSyncServerPreference();
try { try {
// There are error states determined by Android, not the login state // There are error states determined by Android, not the login state
@ -417,6 +452,38 @@ public class FxAccountStatusFragment
deviceNamePreference.setText(clientName); deviceNamePreference.setText(clientName);
} }
protected void updateAuthServerPreference() {
final String authServer = fxAccount.getAccountServerURI();
final boolean shouldBeShown = ALWAYS_SHOW_AUTH_SERVER || !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServer);
final boolean currentlyShown = null != findPreference(authServerPreference.getKey());
if (currentlyShown != shouldBeShown) {
if (shouldBeShown) {
accountCategory.addPreference(authServerPreference);
} else {
accountCategory.removePreference(authServerPreference);
}
}
// Always set the summary, because on first run, the preference is visible,
// and the above block will be skipped if there is a custom value.
authServerPreference.setSummary(authServer);
}
protected void updateSyncServerPreference() {
final String syncServer = fxAccount.getTokenServerURI();
final boolean shouldBeShown = ALWAYS_SHOW_SYNC_SERVER || !FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServer);
final boolean currentlyShown = null != findPreference(syncServerPreference.getKey());
if (currentlyShown != shouldBeShown) {
if (shouldBeShown) {
syncCategory.addPreference(syncServerPreference);
} else {
syncCategory.removePreference(syncServerPreference);
}
}
// Always set the summary, because on first run, the preference is visible,
// and the above block will be skipped if there is a custom value.
syncServerPreference.setSummary(syncServer);
}
/** /**
* Query shared prefs for the current engine state, and update the UI * Query shared prefs for the current engine state, and update the UI
* accordingly. * accordingly.

Просмотреть файл

@ -77,6 +77,8 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
TextView view = (TextView) findViewById(R.id.forgot_password_link); TextView view = (TextView) findViewById(R.id.forgot_password_link);
ActivityUtils.linkTextView(view, R.string.fxaccount_sign_in_forgot_password, R.string.fxaccount_link_forgot_password); ActivityUtils.linkTextView(view, R.string.fxaccount_sign_in_forgot_password, R.string.fxaccount_link_forgot_password);
updateFromIntentExtras();
} }
@Override @Override

Просмотреть файл

@ -56,18 +56,18 @@ public class AccountPickler {
public static final long PICKLE_VERSION = 2; public static final long PICKLE_VERSION = 2;
private static final String KEY_PICKLE_VERSION = "pickle_version"; public static final String KEY_PICKLE_VERSION = "pickle_version";
private static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp"; public static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp";
private static final String KEY_ACCOUNT_VERSION = "account_version"; public static final String KEY_ACCOUNT_VERSION = "account_version";
private static final String KEY_ACCOUNT_TYPE = "account_type"; public static final String KEY_ACCOUNT_TYPE = "account_type";
private static final String KEY_EMAIL = "email"; public static final String KEY_EMAIL = "email";
private static final String KEY_PROFILE = "profile"; public static final String KEY_PROFILE = "profile";
private static final String KEY_IDP_SERVER_URI = "idpServerURI"; public static final String KEY_IDP_SERVER_URI = "idpServerURI";
private static final String KEY_TOKEN_SERVER_URI = "tokenServerURI"; public static final String KEY_TOKEN_SERVER_URI = "tokenServerURI";
private static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled"; public static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled";
private static final String KEY_BUNDLE = "bundle"; public static final String KEY_BUNDLE = "bundle";
/** /**
* Remove Firefox account persisted to disk. * Remove Firefox account persisted to disk.
@ -80,16 +80,10 @@ public class AccountPickler {
return context.deleteFile(filename); return context.deleteFile(filename);
} }
/** public static ExtendedJSONObject toJSON(final AndroidFxAccount account, final long now) {
* Persist Firefox account to disk as a JSON object.
*
* @param AndroidFxAccount the account to persist to disk
* @param filename name of file to persist to; must not contain path separators.
*/
public static void pickle(final AndroidFxAccount account, final String filename) {
final ExtendedJSONObject o = new ExtendedJSONObject(); final ExtendedJSONObject o = new ExtendedJSONObject();
o.put(KEY_PICKLE_VERSION, Long.valueOf(PICKLE_VERSION)); o.put(KEY_PICKLE_VERSION, Long.valueOf(PICKLE_VERSION));
o.put(KEY_PICKLE_TIMESTAMP, Long.valueOf(System.currentTimeMillis())); o.put(KEY_PICKLE_TIMESTAMP, Long.valueOf(now));
o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION); o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE); o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
@ -104,10 +98,21 @@ public class AccountPickler {
final ExtendedJSONObject bundle = account.unbundle(); final ExtendedJSONObject bundle = account.unbundle();
if (bundle == null) { if (bundle == null) {
Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting."); Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting.");
return; return null;
} }
o.put(KEY_BUNDLE, bundle); o.put(KEY_BUNDLE, bundle);
return o;
}
/**
* Persist Firefox account to disk as a JSON object.
*
* @param AndroidFxAccount the account to persist to disk
* @param filename name of file to persist to; must not contain path separators.
*/
public static void pickle(final AndroidFxAccount account, final String filename) {
final ExtendedJSONObject o = toJSON(account, System.currentTimeMillis());
writeToDisk(account.context, filename, o); writeToDisk(account.context, filename, o);
} }

Просмотреть файл

@ -323,6 +323,9 @@ public class AndroidFxAccount {
if (email == null) { if (email == null) {
throw new IllegalArgumentException("email must not be null"); throw new IllegalArgumentException("email must not be null");
} }
if (profile == null) {
throw new IllegalArgumentException("profile must not be null");
}
if (idpServerURI == null) { if (idpServerURI == null) {
throw new IllegalArgumentException("idpServerURI must not be null"); throw new IllegalArgumentException("idpServerURI must not be null");
} }
@ -368,6 +371,15 @@ public class AndroidFxAccount {
return null; return null;
} }
// Try to work around an intermittent issue described at
// http://stackoverflow.com/a/11698139. What happens is that tests that
// delete and re-create the same account frequently will find the account
// missing all or some of the userdata bundle, possibly due to an Android
// AccountManager caching bug.
for (String key : userdata.keySet()) {
accountManager.setUserData(account, key, userdata.getString(key));
}
AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
if (!fromPickle) { if (!fromPickle) {

Просмотреть файл

@ -5,23 +5,81 @@
package org.mozilla.gecko.home; package org.mozilla.gecko.home;
import org.mozilla.gecko.R; import java.util.Date;
import org.mozilla.gecko.widget.IconTabWidget; import java.util.EnumSet;
import android.content.res.Configuration; import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserDB.URLColumns;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.SparseArray;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.TextView;
public class HistoryPanel extends HomeFragment /**
implements IconTabWidget.OnTabChangedListener { * Fragment that displays recent history in a ListView.
*/
public class HistoryPanel extends HomeFragment {
// Logging tag name // Logging tag name
private static final String LOGTAG = "GeckoHistoryPanel"; private static final String LOGTAG = "GeckoHistoryPanel";
private IconTabWidget mTabWidget;
private int mSelectedTab; // Cursor loader ID for history query
private boolean initializeRecentPanel; private static final int LOADER_ID_HISTORY = 0;
// Adapter for the list of recent history entries.
private HistoryAdapter mAdapter;
// The view shown by the fragment.
private HomeListView mList;
// The button view for clearing browsing history.
private View mClearHistoryButton;
// Reference to the View to display when there are no results.
private View mEmptyView;
// Callbacks used for the search and favicon cursor loaders
private CursorLoaderCallbacks mCursorLoaderCallbacks;
// On URL open listener
private OnUrlOpenListener mUrlOpenListener;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mUrlOpenListener = (OnUrlOpenListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement HomePager.OnUrlOpenListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mUrlOpenListener = null;
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -30,75 +88,335 @@ public class HistoryPanel extends HomeFragment
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState) { public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); mList = (HomeListView) view.findViewById(R.id.list);
mList.setTag(HomePager.LIST_TAG_HISTORY);
mTabWidget = (IconTabWidget) view.findViewById(R.id.tab_icon_widget); mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
position -= mAdapter.getMostRecentSectionsCountBefore(position);
final Cursor c = mAdapter.getCursor(position);
final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
mTabWidget.addTab(R.drawable.icon_most_recent, R.string.home_most_recent_title); Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL);
mTabWidget.addTab(R.drawable.icon_last_tabs, R.string.home_last_tabs_title);
mTabWidget.setTabSelectionListener(this); // This item is a TwoLinePageRow, so we allow switch-to-tab.
mTabWidget.setCurrentTab(mSelectedTab); mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
}
});
mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
info.display = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY));
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID));
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID);
if (cursor.isNull(bookmarkIdCol)) {
// If this is a combined cursor, we may get a history item without a
// bookmark, in which case the bookmarks ID column value will be null.
info.bookmarkId = -1;
} else {
info.bookmarkId = cursor.getInt(bookmarkIdCol);
}
return info;
}
});
registerForContextMenu(mList);
mClearHistoryButton = view.findViewById(R.id.clear_history_button);
mClearHistoryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final Context context = getActivity();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
});
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final ContentResolver cr = context.getContentResolver();
BrowserDB.clearHistory(cr);
}
});
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
}
});
dialogBuilder.show();
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
mList = null;
mEmptyView = null;
mClearHistoryButton = null;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Intialize adapter
mAdapter = new HistoryAdapter(getActivity());
mList.setAdapter(mAdapter);
// Create callbacks before the initial loader is started
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
loadIfVisible(); loadIfVisible();
} }
@Override @Override
public void load() { protected void load() {
// Show most recent panel as the initial panel. getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
// Since we detach/attach on config change, this prevents from replacing current fragment.
if (!initializeRecentPanel) {
showMostRecentPanel();
initializeRecentPanel = true;
} }
private static class HistoryCursorLoader extends SimpleCursorLoader {
// Max number of history results
private static final int HISTORY_LIMIT = 100;
public HistoryCursorLoader(Context context) {
super(context);
} }
@Override @Override
public void onTabChanged(int index) { public Cursor loadCursor() {
if (index == mSelectedTab) { final ContentResolver cr = getContext().getContentResolver();
return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT);
}
}
private void updateUiFromCursor(Cursor c) {
if (c != null && c.getCount() > 0) {
mClearHistoryButton.setVisibility(View.VISIBLE);
return; return;
} }
if (index == 0) { // Cursor is empty, so hide the "Clear browsing history" button,
showMostRecentPanel(); // and set the empty view if it hasn't been set already.
} else if (index == 1) { mClearHistoryButton.setVisibility(View.GONE);
showLastTabsPanel();
if (mEmptyView == null) {
// Set empty panel view. We delay this so that the empty view won't flash.
final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
mEmptyView = emptyViewStub.inflate();
final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
emptyIcon.setImageResource(R.drawable.icon_most_recent_empty);
final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
emptyText.setText(R.string.home_most_recent_empty);
mList.setEmptyView(mEmptyView);
}
} }
mTabWidget.setCurrentTab(index); private static class HistoryAdapter extends MultiTypeCursorAdapter {
mSelectedTab = index; private static final int ROW_HEADER = 0;
private static final int ROW_STANDARD = 1;
private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER };
private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row };
// For the time sections in history
private static final long MS_PER_DAY = 86400000;
private static final long MS_PER_WEEK = MS_PER_DAY * 7;
// The time ranges for each section
private static enum MostRecentSection {
TODAY,
YESTERDAY,
WEEK,
OLDER
};
private final Context mContext;
// Maps headers in the list with their respective sections
private final SparseArray<MostRecentSection> mMostRecentSections;
public HistoryAdapter(Context context) {
super(context, null, VIEW_TYPES, LAYOUT_TYPES);
mContext = context;
// Initialize map of history sections
mMostRecentSections = new SparseArray<MostRecentSection>();
} }
@Override @Override
public void onConfigurationChanged(Configuration newConfig) { public Object getItem(int position) {
super.onConfigurationChanged(newConfig); final int type = getItemViewType(position);
// Rotation should detach and re-attach to use a different layout. // Header items are not in the cursor
if (isVisible()) { if (type == ROW_HEADER) {
getFragmentManager().beginTransaction() return null;
.detach(this) }
.attach(this)
.commitAllowingStateLoss(); return super.getItem(position - getMostRecentSectionsCountBefore(position));
}
@Override
public int getItemViewType(int position) {
if (mMostRecentSections.get(position) != null) {
return ROW_HEADER;
}
return ROW_STANDARD;
}
@Override
public boolean isEnabled(int position) {
return (getItemViewType(position) == ROW_STANDARD);
}
@Override
public int getCount() {
// Add the history section headers to the number of reported results.
return super.getCount() + mMostRecentSections.size();
}
@Override
public Cursor swapCursor(Cursor cursor) {
loadMostRecentSections(cursor);
Cursor oldCursor = super.swapCursor(cursor);
return oldCursor;
}
@Override
public void bindView(View view, Context context, int position) {
final int type = getItemViewType(position);
if (type == ROW_HEADER) {
final MostRecentSection section = mMostRecentSections.get(position);
final TextView row = (TextView) view;
row.setText(getMostRecentSectionTitle(section));
} else {
// Account for the most recent section headers
position -= getMostRecentSectionsCountBefore(position);
final Cursor c = getCursor(position);
final TwoLinePageRow row = (TwoLinePageRow) view;
row.updateFromCursor(c);
} }
} }
private void showSubPanel(Fragment subPanel) { private String getMostRecentSectionTitle(MostRecentSection section) {
final Bundle args = new Bundle(); switch (section) {
args.putBoolean(HomePager.CAN_LOAD_ARG, getCanLoadHint()); case TODAY:
subPanel.setArguments(args); return mContext.getString(R.string.history_today_section);
case YESTERDAY:
getChildFragmentManager().beginTransaction() return mContext.getString(R.string.history_yesterday_section);
.addToBackStack(null).replace(R.id.history_panel_container, subPanel) case WEEK:
.commitAllowingStateLoss(); return mContext.getString(R.string.history_week_section);
case OLDER:
return mContext.getString(R.string.history_older_section);
} }
private void showMostRecentPanel() { throw new IllegalStateException("Unrecognized history section");
final MostRecentPanel mostRecentPanel = MostRecentPanel.newInstance();
showSubPanel(mostRecentPanel);
} }
private void showLastTabsPanel() { private int getMostRecentSectionsCountBefore(int position) {
final LastTabsPanel lastTabsPanel = LastTabsPanel.newInstance(); // Account for the number headers before the given position
showSubPanel(lastTabsPanel); int sectionsBefore = 0;
final int historySectionsCount = mMostRecentSections.size();
for (int i = 0; i < historySectionsCount; i++) {
final int sectionPosition = mMostRecentSections.keyAt(i);
if (sectionPosition > position) {
break;
}
sectionsBefore++;
}
return sectionsBefore;
}
private static MostRecentSection getMostRecentSectionForTime(long from, long time) {
long delta = from - time;
if (delta < 0) {
return MostRecentSection.TODAY;
}
if (delta < MS_PER_DAY) {
return MostRecentSection.YESTERDAY;
}
if (delta < MS_PER_WEEK) {
return MostRecentSection.WEEK;
}
return MostRecentSection.OLDER;
}
private void loadMostRecentSections(Cursor c) {
// Clear any history sections that may have been loaded before.
mMostRecentSections.clear();
if (c == null || !c.moveToFirst()) {
return;
}
final Date now = new Date();
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
final long today = now.getTime();
MostRecentSection section = null;
do {
final int position = c.getPosition();
final long time = c.getLong(c.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED));
final MostRecentSection itemSection = HistoryAdapter.getMostRecentSectionForTime(today, time);
if (section != itemSection) {
section = itemSection;
mMostRecentSections.append(position + mMostRecentSections.size(), section);
}
// Reached the last section, no need to continue
if (section == MostRecentSection.OLDER) {
break;
}
} while (c.moveToNext());
}
}
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new HistoryCursorLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
mAdapter.swapCursor(c);
updateUiFromCursor(c);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
} }
} }

Просмотреть файл

@ -40,6 +40,7 @@ public final class HomeConfig {
BOOKMARKS("bookmarks", BookmarksPanel.class), BOOKMARKS("bookmarks", BookmarksPanel.class),
HISTORY("history", HistoryPanel.class), HISTORY("history", HistoryPanel.class),
READING_LIST("reading_list", ReadingListPanel.class), READING_LIST("reading_list", ReadingListPanel.class),
RECENT_TABS("recent_tabs", RecentTabsPanel.class),
DYNAMIC("dynamic", DynamicPanel.class); DYNAMIC("dynamic", DynamicPanel.class);
private final String mId; private final String mId;
@ -1495,6 +1496,7 @@ public final class HomeConfig {
private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907"; private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907";
private static final String READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b"; private static final String READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8"; private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8";
private final HomeConfigBackend mBackend; private final HomeConfigBackend mBackend;
@ -1550,6 +1552,11 @@ public final class HomeConfig {
id = READING_LIST_PANEL_ID; id = READING_LIST_PANEL_ID;
break; break;
case RECENT_TABS:
titleId = R.string.recent_tabs_title;
id = RECENT_TABS_PANEL_ID;
break;
case DYNAMIC: case DYNAMIC:
throw new IllegalArgumentException("createBuiltinPanelConfig() is only for built-in panels"); throw new IllegalArgumentException("createBuiltinPanelConfig() is only for built-in panels");
} }

Просмотреть файл

@ -36,7 +36,19 @@ import android.util.Log;
class HomeConfigPrefsBackend implements HomeConfigBackend { class HomeConfigPrefsBackend implements HomeConfigBackend {
private static final String LOGTAG = "GeckoHomeConfigBackend"; private static final String LOGTAG = "GeckoHomeConfigBackend";
private static final String PREFS_CONFIG_KEY = "home_panels"; // Increment this to trigger a migration.
private static final int VERSION = 1;
// This key was originally used to store only an array of panel configs.
private static final String PREFS_CONFIG_KEY_OLD = "home_panels";
// This key is now used to store a version number with the array of panel configs.
private static final String PREFS_CONFIG_KEY = "home_panels_with_version";
// Keys used with JSON object stored in prefs.
private static final String JSON_KEY_PANELS = "panels";
private static final String JSON_KEY_VERSION = "version";
private static final String PREFS_LOCALE_KEY = "home_locale"; private static final String PREFS_LOCALE_KEY = "home_locale";
private static final String RELOAD_BROADCAST = "HomeConfigPrefsBackend:Reload"; private static final String RELOAD_BROADCAST = "HomeConfigPrefsBackend:Reload";
@ -45,6 +57,8 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
private ReloadBroadcastReceiver mReloadBroadcastReceiver; private ReloadBroadcastReceiver mReloadBroadcastReceiver;
private OnReloadListener mReloadListener; private OnReloadListener mReloadListener;
private static boolean sMigrationDone = false;
public HomeConfigPrefsBackend(Context context) { public HomeConfigPrefsBackend(Context context) {
mContext = context; mContext = context;
} }
@ -68,22 +82,109 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
} }
final PanelConfig historyEntry = createBuiltinPanelConfig(mContext, PanelType.HISTORY); final PanelConfig historyEntry = createBuiltinPanelConfig(mContext, PanelType.HISTORY);
final PanelConfig recentTabsEntry = createBuiltinPanelConfig(mContext, PanelType.RECENT_TABS);
// On tablets, the history panel is the last. // On tablets, the history panel is the last.
// On phones, the history panel is the first one. // On phones, the history panel is the first one.
if (HardwareUtils.isTablet()) { if (HardwareUtils.isTablet()) {
panelConfigs.add(historyEntry); panelConfigs.add(historyEntry);
panelConfigs.add(recentTabsEntry);
} else { } else {
panelConfigs.add(0, historyEntry); panelConfigs.add(0, historyEntry);
panelConfigs.add(0, recentTabsEntry);
} }
return new State(panelConfigs, true); return new State(panelConfigs, true);
} }
/**
* Migrates JSON config data storage.
*
* @param context Context used to get shared preferences and create built-in panel.
* @param jsonString String currently stored in preferences.
*
* @return JSONArray array representing new set of panel configs.
*/
private static synchronized JSONArray maybePerformMigration(Context context, String jsonString) throws JSONException {
// If the migration is already done, we're at the current version.
if (sMigrationDone) {
final JSONObject json = new JSONObject(jsonString);
return json.getJSONArray(JSON_KEY_PANELS);
}
// Make sure we only do this version check once.
sMigrationDone = true;
final JSONArray originalJsonPanels;
final int version;
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
if (prefs.contains(PREFS_CONFIG_KEY_OLD)) {
// Our original implementation did not contain versioning, so this is implicitly version 0.
originalJsonPanels = new JSONArray(jsonString);
version = 0;
} else {
final JSONObject json = new JSONObject(jsonString);
originalJsonPanels = json.getJSONArray(JSON_KEY_PANELS);
version = json.getInt(JSON_KEY_VERSION);
}
if (version == VERSION) {
return originalJsonPanels;
}
Log.d(LOGTAG, "Performing migration");
final JSONArray newJsonPanels = new JSONArray();
final SharedPreferences.Editor prefsEditor = prefs.edit();
for (int v = version + 1; v <= VERSION; v++) {
Log.d(LOGTAG, "Migrating to version = " + v);
switch (v) {
case 1:
// Add "Recent Tabs" panel
final PanelConfig recentTabsConfig = createBuiltinPanelConfig(context, PanelType.RECENT_TABS);
final JSONObject jsonRecentTabsConfig = recentTabsConfig.toJSON();
// Add the new panel to the front of the array on phones.
if (!HardwareUtils.isTablet()) {
newJsonPanels.put(jsonRecentTabsConfig);
}
// Copy the original panel configs.
final int count = originalJsonPanels.length();
for (int i = 0; i < count; i++) {
final JSONObject jsonPanelConfig = originalJsonPanels.getJSONObject(i);
newJsonPanels.put(jsonPanelConfig);
}
// Add the new panel to the end of the array on tablets.
if (HardwareUtils.isTablet()) {
newJsonPanels.put(jsonRecentTabsConfig);
}
// Remove the old pref key.
prefsEditor.remove(PREFS_CONFIG_KEY_OLD);
break;
}
}
// Save the new panel config and the new version number.
final JSONObject newJson = new JSONObject();
newJson.put(JSON_KEY_PANELS, newJsonPanels);
newJson.put(JSON_KEY_VERSION, VERSION);
prefsEditor.putString(PREFS_CONFIG_KEY, newJson.toString());
prefsEditor.commit();
return newJsonPanels;
}
private State loadConfigFromString(String jsonString) { private State loadConfigFromString(String jsonString) {
final JSONArray jsonPanelConfigs; final JSONArray jsonPanelConfigs;
try { try {
jsonPanelConfigs = new JSONArray(jsonString); jsonPanelConfigs = maybePerformMigration(mContext, jsonString);
} catch (JSONException e) { } catch (JSONException e) {
Log.e(LOGTAG, "Error loading the list of home panels from JSON prefs", e); Log.e(LOGTAG, "Error loading the list of home panels from JSON prefs", e);
@ -110,7 +211,9 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
@Override @Override
public State load() { public State load() {
final SharedPreferences prefs = getSharedPreferences(); final SharedPreferences prefs = getSharedPreferences();
final String jsonString = prefs.getString(PREFS_CONFIG_KEY, null);
final String key = (prefs.contains(PREFS_CONFIG_KEY_OLD) ? PREFS_CONFIG_KEY_OLD : PREFS_CONFIG_KEY);
final String jsonString = prefs.getString(key, null);
final State configState; final State configState;
if (TextUtils.isEmpty(jsonString)) { if (TextUtils.isEmpty(jsonString)) {
@ -142,7 +245,15 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
} }
} }
editor.putString(PREFS_CONFIG_KEY, jsonPanelConfigs.toString()); try {
final JSONObject json = new JSONObject();
json.put(JSON_KEY_PANELS, jsonPanelConfigs);
json.put(JSON_KEY_VERSION, VERSION);
editor.putString(PREFS_CONFIG_KEY, json.toString());
} catch (JSONException e) {
Log.e(LOGTAG, "Exception saving PanelConfig state", e);
}
} }
editor.putString(PREFS_LOCALE_KEY, Locale.getDefault().toString()); editor.putString(PREFS_LOCALE_KEY, Locale.getDefault().toString());

Просмотреть файл

@ -68,8 +68,7 @@ public class HomePager extends ViewPager {
static final String LIST_TAG_BOOKMARKS = "bookmarks"; static final String LIST_TAG_BOOKMARKS = "bookmarks";
static final String LIST_TAG_READING_LIST = "reading_list"; static final String LIST_TAG_READING_LIST = "reading_list";
static final String LIST_TAG_TOP_SITES = "top_sites"; static final String LIST_TAG_TOP_SITES = "top_sites";
static final String LIST_TAG_MOST_RECENT = "most_recent"; static final String LIST_TAG_RECENT_TABS = "recent_tabs";
static final String LIST_TAG_LAST_TABS = "last_tabs";
static final String LIST_TAG_BROWSER_SEARCH = "browser_search"; static final String LIST_TAG_BROWSER_SEARCH = "browser_search";
public interface OnUrlOpenListener { public interface OnUrlOpenListener {

Просмотреть файл

@ -1,430 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import java.util.Date;
import java.util.EnumSet;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserDB.URLColumns;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.TextView;
/**
* Fragment that displays recent history in a ListView.
*/
public class MostRecentPanel extends HomeFragment {
// Logging tag name
private static final String LOGTAG = "GeckoMostRecentPanel";
// Cursor loader ID for history query
private static final int LOADER_ID_HISTORY = 0;
// Adapter for the list of search results
private MostRecentAdapter mAdapter;
// The view shown by the fragment.
private HomeListView mList;
// The button view for clearing browsing history.
private View mClearHistoryButton;
// Reference to the View to display when there are no results.
private View mEmptyView;
// Callbacks used for the search and favicon cursor loaders
private CursorLoaderCallbacks mCursorLoaderCallbacks;
// On URL open listener
private OnUrlOpenListener mUrlOpenListener;
public static MostRecentPanel newInstance() {
return new MostRecentPanel();
}
public MostRecentPanel() {
mUrlOpenListener = null;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mUrlOpenListener = (OnUrlOpenListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement HomePager.OnUrlOpenListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mUrlOpenListener = null;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.home_most_recent_panel, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mList = (HomeListView) view.findViewById(R.id.list);
mList.setTag(HomePager.LIST_TAG_MOST_RECENT);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
position -= mAdapter.getMostRecentSectionsCountBefore(position);
final Cursor c = mAdapter.getCursor(position);
final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL);
// This item is a TwoLinePageRow, so we allow switch-to-tab.
mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
}
});
mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
info.display = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY));
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID));
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID);
if (cursor.isNull(bookmarkIdCol)) {
// If this is a combined cursor, we may get a history item without a
// bookmark, in which case the bookmarks ID column value will be null.
info.bookmarkId = -1;
} else {
info.bookmarkId = cursor.getInt(bookmarkIdCol);
}
return info;
}
});
registerForContextMenu(mList);
mClearHistoryButton = view.findViewById(R.id.clear_history_button);
mClearHistoryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final Context context = getActivity();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
});
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final ContentResolver cr = context.getContentResolver();
BrowserDB.clearHistory(cr);
}
});
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
}
});
dialogBuilder.show();
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
mList = null;
mEmptyView = null;
mClearHistoryButton = null;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Intialize adapter
mAdapter = new MostRecentAdapter(getActivity());
mList.setAdapter(mAdapter);
// Create callbacks before the initial loader is started
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
loadIfVisible();
}
@Override
protected void load() {
getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
}
private static class MostRecentCursorLoader extends SimpleCursorLoader {
// Max number of history results
private static final int HISTORY_LIMIT = 100;
public MostRecentCursorLoader(Context context) {
super(context);
}
@Override
public Cursor loadCursor() {
final ContentResolver cr = getContext().getContentResolver();
return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT);
}
}
private void updateUiFromCursor(Cursor c) {
if (c != null && c.getCount() > 0) {
mClearHistoryButton.setVisibility(View.VISIBLE);
return;
}
// Cursor is empty, so hide the "Clear browsing history" button,
// and set the empty view if it hasn't been set already.
mClearHistoryButton.setVisibility(View.GONE);
if (mEmptyView == null) {
// Set empty panel view. We delay this so that the empty view won't flash.
final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
mEmptyView = emptyViewStub.inflate();
final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
emptyIcon.setImageResource(R.drawable.icon_most_recent_empty);
final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
emptyText.setText(R.string.home_most_recent_empty);
mList.setEmptyView(mEmptyView);
}
}
private static class MostRecentAdapter extends MultiTypeCursorAdapter {
private static final int ROW_HEADER = 0;
private static final int ROW_STANDARD = 1;
private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER };
private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row };
// For the time sections in history
private static final long MS_PER_DAY = 86400000;
private static final long MS_PER_WEEK = MS_PER_DAY * 7;
// The time ranges for each section
private static enum MostRecentSection {
TODAY,
YESTERDAY,
WEEK,
OLDER
};
private final Context mContext;
// Maps headers in the list with their respective sections
private final SparseArray<MostRecentSection> mMostRecentSections;
public MostRecentAdapter(Context context) {
super(context, null, VIEW_TYPES, LAYOUT_TYPES);
mContext = context;
// Initialize map of history sections
mMostRecentSections = new SparseArray<MostRecentSection>();
}
@Override
public Object getItem(int position) {
final int type = getItemViewType(position);
// Header items are not in the cursor
if (type == ROW_HEADER) {
return null;
}
return super.getItem(position - getMostRecentSectionsCountBefore(position));
}
@Override
public int getItemViewType(int position) {
if (mMostRecentSections.get(position) != null) {
return ROW_HEADER;
}
return ROW_STANDARD;
}
@Override
public boolean isEnabled(int position) {
return (getItemViewType(position) == ROW_STANDARD);
}
@Override
public int getCount() {
// Add the history section headers to the number of reported results.
return super.getCount() + mMostRecentSections.size();
}
@Override
public Cursor swapCursor(Cursor cursor) {
loadMostRecentSections(cursor);
Cursor oldCursor = super.swapCursor(cursor);
return oldCursor;
}
@Override
public void bindView(View view, Context context, int position) {
final int type = getItemViewType(position);
if (type == ROW_HEADER) {
final MostRecentSection section = mMostRecentSections.get(position);
final TextView row = (TextView) view;
row.setText(getMostRecentSectionTitle(section));
} else {
// Account for the most recent section headers
position -= getMostRecentSectionsCountBefore(position);
final Cursor c = getCursor(position);
final TwoLinePageRow row = (TwoLinePageRow) view;
row.updateFromCursor(c);
}
}
private String getMostRecentSectionTitle(MostRecentSection section) {
switch (section) {
case TODAY:
return mContext.getString(R.string.history_today_section);
case YESTERDAY:
return mContext.getString(R.string.history_yesterday_section);
case WEEK:
return mContext.getString(R.string.history_week_section);
case OLDER:
return mContext.getString(R.string.history_older_section);
}
throw new IllegalStateException("Unrecognized history section");
}
private int getMostRecentSectionsCountBefore(int position) {
// Account for the number headers before the given position
int sectionsBefore = 0;
final int historySectionsCount = mMostRecentSections.size();
for (int i = 0; i < historySectionsCount; i++) {
final int sectionPosition = mMostRecentSections.keyAt(i);
if (sectionPosition > position) {
break;
}
sectionsBefore++;
}
return sectionsBefore;
}
private static MostRecentSection getMostRecentSectionForTime(long from, long time) {
long delta = from - time;
if (delta < 0) {
return MostRecentSection.TODAY;
}
if (delta < MS_PER_DAY) {
return MostRecentSection.YESTERDAY;
}
if (delta < MS_PER_WEEK) {
return MostRecentSection.WEEK;
}
return MostRecentSection.OLDER;
}
private void loadMostRecentSections(Cursor c) {
// Clear any history sections that may have been loaded before.
mMostRecentSections.clear();
if (c == null || !c.moveToFirst()) {
return;
}
final Date now = new Date();
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
final long today = now.getTime();
MostRecentSection section = null;
do {
final int position = c.getPosition();
final long time = c.getLong(c.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED));
final MostRecentSection itemSection = MostRecentAdapter.getMostRecentSectionForTime(today, time);
if (section != itemSection) {
section = itemSection;
mMostRecentSections.append(position + mMostRecentSections.size(), section);
}
// Reached the last section, no need to continue
if (section == MostRecentSection.OLDER) {
break;
}
} while (c.moveToNext());
}
}
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new MostRecentCursorLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
mAdapter.swapCursor(c);
updateUiFromCursor(c);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
}
}

Просмотреть файл

@ -6,16 +6,25 @@
package org.mozilla.gecko.home; package org.mozilla.gecko.home;
import org.mozilla.gecko.AboutPages; import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.SessionParser; import org.mozilla.gecko.SessionParser;
import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.Combined; import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.home.HomePager.OnNewTabsListener; import org.mozilla.gecko.home.HomePager.OnNewTabsListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.database.Cursor; import android.database.Cursor;
import android.database.MatrixCursor; import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder; import android.database.MatrixCursor.RowBuilder;
@ -23,6 +32,7 @@ import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter; import android.support.v4.widget.CursorAdapter;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -34,25 +44,20 @@ import android.widget.TextView;
/** /**
* Fragment that displays tabs from last session in a ListView. * Fragment that displays tabs from last session in a ListView.
*/ */
public class LastTabsPanel extends HomeFragment { public class RecentTabsPanel extends HomeFragment
implements NativeEventListener {
// Logging tag name // Logging tag name
private static final String LOGTAG = "GeckoLastTabsPanel"; private static final String LOGTAG = "GeckoRecentTabsPanel";
// Cursor loader ID for the session parser // Cursor loader ID for the loader that loads recent tabs
private static final int LOADER_ID_LAST_TABS = 0; private static final int LOADER_ID_RECENT_TABS = 0;
// Adapter for the list of search results // Adapter for the list of recent tabs.
private LastTabsAdapter mAdapter; private RecentTabsAdapter mAdapter;
// The view shown by the fragment. // The view shown by the fragment.
private HomeListView mList; private HomeListView mList;
// The title for this HomeFragment panel.
private TextView mTitle;
// The button view for restoring tabs from last session.
private View mRestoreButton;
// Reference to the View to display when there are no results. // Reference to the View to display when there are no results.
private View mEmptyView; private View mEmptyView;
@ -62,12 +67,25 @@ public class LastTabsPanel extends HomeFragment {
// On new tabs listener // On new tabs listener
private OnNewTabsListener mNewTabsListener; private OnNewTabsListener mNewTabsListener;
public static LastTabsPanel newInstance() { // Recently closed tabs from gecko
return new LastTabsPanel(); private ClosedTab[] mClosedTabs;
private static final class ClosedTab {
public final String url;
public final String title;
public ClosedTab(String url, String title) {
this.url = url;
this.title = title;
}
} }
public LastTabsPanel() { public static final class RecentTabs implements URLColumns, CommonColumns {
mNewTabsListener = null; public static final String TYPE = "type";
public static final int TYPE_HEADER = 0;
public static final int TYPE_LAST_TIME = 1;
public static final int TYPE_CLOSED = 2;
} }
@Override @Override
@ -91,18 +109,13 @@ public class LastTabsPanel extends HomeFragment {
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.home_last_tabs_panel, container, false); return inflater.inflate(R.layout.home_recent_tabs_panel, container, false);
} }
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState) { public void onViewCreated(View view, Bundle savedInstanceState) {
mTitle = (TextView) view.findViewById(R.id.title);
if (mTitle != null) {
mTitle.setText(R.string.home_last_tabs_title);
}
mList = (HomeListView) view.findViewById(R.id.list); mList = (HomeListView) view.findViewById(R.id.list);
mList.setTag(HomePager.LIST_TAG_LAST_TABS); mList.setTag(HomePager.LIST_TAG_RECENT_TABS);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
@ -114,7 +127,7 @@ public class LastTabsPanel extends HomeFragment {
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL); Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL);
final String url = c.getString(c.getColumnIndexOrThrow(Combined.URL)); final String url = c.getString(c.getColumnIndexOrThrow(RecentTabs.URL));
mNewTabsListener.onNewTabs(new String[] { url }); mNewTabsListener.onNewTabs(new String[] { url });
} }
}); });
@ -123,30 +136,39 @@ public class LastTabsPanel extends HomeFragment {
@Override @Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL)); info.url = cursor.getString(cursor.getColumnIndexOrThrow(RecentTabs.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE)); info.title = cursor.getString(cursor.getColumnIndexOrThrow(RecentTabs.TITLE));
return info; return info;
} }
}); });
registerForContextMenu(mList); registerForContextMenu(mList);
mRestoreButton = view.findViewById(R.id.open_all_tabs_button); EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
mRestoreButton.setOnClickListener(new View.OnClickListener() { GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("ClosedTabs:StartNotifications", null));
@Override
public void onClick(View v) {
openAllTabs();
}
});
} }
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
mList = null; mList = null;
mTitle = null;
mEmptyView = null; mEmptyView = null;
mRestoreButton = null;
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("ClosedTabs:StopNotifications", null));
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Detach and reattach the fragment as the layout changes.
if (isVisible()) {
getFragmentManager().beginTransaction()
.detach(this)
.attach(this)
.commitAllowingStateLoss();
}
} }
@Override @Override
@ -154,7 +176,7 @@ public class LastTabsPanel extends HomeFragment {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
// Intialize adapter // Intialize adapter
mAdapter = new LastTabsAdapter(getActivity()); mAdapter = new RecentTabsAdapter(getActivity());
mList.setAdapter(mAdapter); mList.setAdapter(mAdapter);
// Create callbacks before the initial loader is started // Create callbacks before the initial loader is started
@ -164,20 +186,9 @@ public class LastTabsPanel extends HomeFragment {
private void updateUiFromCursor(Cursor c) { private void updateUiFromCursor(Cursor c) {
if (c != null && c.getCount() > 0) { if (c != null && c.getCount() > 0) {
if (mTitle != null) {
mTitle.setVisibility(View.VISIBLE);
}
mRestoreButton.setVisibility(View.VISIBLE);
return; return;
} }
// Cursor is empty, so hide the title and set the
// empty view if it hasn't been set already.
if (mTitle != null) {
mTitle.setVisibility(View.GONE);
}
mRestoreButton.setVisibility(View.GONE);
if (mEmptyView == null) { if (mEmptyView == null) {
// Set empty panel view. We delay this so that the empty view won't flash. // Set empty panel view. We delay this so that the empty view won't flash.
final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub); final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
@ -195,7 +206,32 @@ public class LastTabsPanel extends HomeFragment {
@Override @Override
protected void load() { protected void load() {
getLoaderManager().initLoader(LOADER_ID_LAST_TABS, null, mCursorLoaderCallbacks); getLoaderManager().initLoader(LOADER_ID_RECENT_TABS, null, mCursorLoaderCallbacks);
}
@Override
public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
final NativeJSObject[] tabs = message.getObjectArray("tabs");
final int length = tabs.length;
final ClosedTab[] closedTabs = new ClosedTab[length];
for (int i = 0; i < length; i++) {
final NativeJSObject tab = tabs[i];
closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"));
}
// Only modify mClosedTabs on the UI thread
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
mClosedTabs = closedTabs;
// Reload the cursor to show recently closed tabs.
if (mClosedTabs.length > 0 && canLoad()) {
getLoaderManager().restartLoader(LOADER_ID_RECENT_TABS, null, mCursorLoaderCallbacks);
}
}
});
} }
private void openAllTabs() { private void openAllTabs() {
@ -207,7 +243,7 @@ public class LastTabsPanel extends HomeFragment {
final String[] urls = new String[c.getCount()]; final String[] urls = new String[c.getCount()];
do { do {
urls[c.getPosition()] = c.getString(c.getColumnIndexOrThrow(Combined.URL)); urls[c.getPosition()] = c.getString(c.getColumnIndexOrThrow(RecentTabs.URL));
} while (c.moveToNext()); } while (c.moveToNext());
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON); Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON);
@ -215,24 +251,52 @@ public class LastTabsPanel extends HomeFragment {
mNewTabsListener.onNewTabs(urls); mNewTabsListener.onNewTabs(urls);
} }
private static class LastTabsCursorLoader extends SimpleCursorLoader { private static class RecentTabsCursorLoader extends SimpleCursorLoader {
public LastTabsCursorLoader(Context context) { private final ClosedTab[] closedTabs;
public RecentTabsCursorLoader(Context context, ClosedTab[] closedTabs) {
super(context); super(context);
this.closedTabs = closedTabs;
}
private void addRow(MatrixCursor c, String url, String title, int type) {
final RowBuilder row = c.newRow();
row.add(-1);
row.add(url);
row.add(title);
row.add(type);
} }
@Override @Override
public Cursor loadCursor() { public Cursor loadCursor() {
final Context context = getContext(); final Context context = getContext();
final MatrixCursor c = new MatrixCursor(new String[] { RecentTabs._ID,
RecentTabs.URL,
RecentTabs.TITLE,
RecentTabs.TYPE });
if (closedTabs != null && closedTabs.length > 0) {
addRow(c, null, context.getString(R.string.home_closed_tabs_title), RecentTabs.TYPE_HEADER);
final int length = closedTabs.length;
for (int i = 0; i < length; i++) {
final String url = closedTabs[i].url;
// Don't show recent tabs for about:home
if (!AboutPages.isAboutHome(url)) {
addRow(c, url, closedTabs[i].title, RecentTabs.TYPE_CLOSED);
}
}
}
final String jsonString = GeckoProfile.get(context).readSessionFile(true); final String jsonString = GeckoProfile.get(context).readSessionFile(true);
if (jsonString == null) { if (jsonString == null) {
// No previous session data // No previous session data
return null; return c;
} }
final MatrixCursor c = new MatrixCursor(new String[] { Combined._ID, final int count = c.getCount();
Combined.URL,
Combined.TITLE });
new SessionParser() { new SessionParser() {
@Override @Override
@ -244,12 +308,12 @@ public class LastTabsPanel extends HomeFragment {
return; return;
} }
final RowBuilder row = c.newRow(); // If this is the first tab we're reading, add a header.
row.add(-1); if (c.getCount() == count) {
row.add(url); addRow(c, null, context.getString(R.string.home_last_tabs_title), RecentTabs.TYPE_HEADER);
}
final String title = tab.getTitle(); addRow(c, url, tab.getTitle(), RecentTabs.TYPE_LAST_TIME);
row.add(title);
} }
}.parse(jsonString); }.parse(jsonString);
@ -257,26 +321,52 @@ public class LastTabsPanel extends HomeFragment {
} }
} }
private static class LastTabsAdapter extends CursorAdapter { private static class RecentTabsAdapter extends MultiTypeCursorAdapter {
public LastTabsAdapter(Context context) { private static final int ROW_HEADER = 0;
super(context, null, 0); private static final int ROW_STANDARD = 1;
private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER };
private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row };
public RecentTabsAdapter(Context context) {
super(context, null, VIEW_TYPES, LAYOUT_TYPES);
}
public int getItemViewType(int position) {
final Cursor c = getCursor(position);
final int type = c.getInt(c.getColumnIndexOrThrow(RecentTabs.TYPE));
if (type == RecentTabs.TYPE_HEADER) {
return ROW_HEADER;
}
return ROW_STANDARD;
}
public boolean isEnabled(int position) {
return (getItemViewType(position) != ROW_HEADER);
} }
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, int position) {
((TwoLinePageRow) view).updateFromCursor(cursor); final int itemType = getItemViewType(position);
} final Cursor c = getCursor(position);
@Override if (itemType == ROW_HEADER) {
public View newView(Context context, Cursor cursor, ViewGroup parent) { final String title = c.getString(c.getColumnIndexOrThrow(RecentTabs.TITLE));
return LayoutInflater.from(context).inflate(R.layout.home_item_row, parent, false); final TextView textView = (TextView) view;
textView.setText(title);
} else if (itemType == ROW_STANDARD) {
final TwoLinePageRow pageRow = (TwoLinePageRow) view;
pageRow.updateFromCursor(c);
}
} }
} }
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
@Override @Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) { public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new LastTabsCursorLoader(getActivity()); return new RecentTabsCursorLoader(getActivity(), mClosedTabs);
} }
@Override @Override

Просмотреть файл

@ -11,6 +11,7 @@
<!ENTITY bookmarks_title "Bookmarks"> <!ENTITY bookmarks_title "Bookmarks">
<!ENTITY history_title "History"> <!ENTITY history_title "History">
<!ENTITY reading_list_title "Reading List"> <!ENTITY reading_list_title "Reading List">
<!ENTITY recent_tabs_title "Recent Tabs">
<!ENTITY switch_to_tab "Switch to tab"> <!ENTITY switch_to_tab "Switch to tab">
@ -349,10 +350,10 @@ size. -->
<!ENTITY home_clear_history_button "Clear browsing history"> <!ENTITY home_clear_history_button "Clear browsing history">
<!ENTITY home_clear_history_confirm "Are you sure you want to clear your history?"> <!ENTITY home_clear_history_confirm "Are you sure you want to clear your history?">
<!ENTITY home_bookmarks_empty "Bookmarks you save show up here."> <!ENTITY home_bookmarks_empty "Bookmarks you save show up here.">
<!ENTITY home_closed_tabs_title "Recently closed tabs">
<!ENTITY home_last_tabs_title "Tabs from last time"> <!ENTITY home_last_tabs_title "Tabs from last time">
<!ENTITY home_last_tabs_open "Open all tabs from last time"> <!ENTITY home_last_tabs_open "Open all tabs from last time">
<!ENTITY home_last_tabs_empty "Your recent tabs show up here."> <!ENTITY home_last_tabs_empty "Your recent tabs show up here.">
<!ENTITY home_most_recent_title "Most recent">
<!ENTITY home_most_recent_empty "Websites you visited most recently show up here."> <!ENTITY home_most_recent_empty "Websites you visited most recently show up here.">
<!ENTITY home_reading_list_empty "Articles you save for later show up here."> <!ENTITY home_reading_list_empty "Articles you save for later show up here.">
<!-- Localization note (home_reading_list_hint): The "TIP" string is synonymous to "hint", "clue", etc. This string is displayed <!-- Localization note (home_reading_list_hint): The "TIP" string is synonymous to "hint", "clue", etc. This string is displayed

Просмотреть файл

@ -109,6 +109,9 @@
<!ENTITY fxaccount_full_label 'Firefox Accounts'> <!ENTITY fxaccount_full_label 'Firefox Accounts'>
<!ENTITY fxaccount_custom_server_account_title 'Using account on server'>
<!ENTITY fxaccount_custom_server_sync_title 'Storing Sync data on server'>
<!-- Localization note: these are shown in all screens that query the <!-- Localization note: these are shown in all screens that query the
user for an email address and password. Hide and show are button user for an email address and password. Hide and show are button
labels. --> labels. -->
@ -175,7 +178,9 @@
<!ENTITY fxaccount_status_header2 'Firefox Account'> <!ENTITY fxaccount_status_header2 'Firefox Account'>
<!ENTITY fxaccount_status_signed_in_as 'Signed in as'> <!ENTITY fxaccount_status_signed_in_as 'Signed in as'>
<!ENTITY fxaccount_status_auth_server 'Account server'>
<!ENTITY fxaccount_status_device_name 'Device name'> <!ENTITY fxaccount_status_device_name 'Device name'>
<!ENTITY fxaccount_status_sync_server 'Sync server'>
<!ENTITY fxaccount_status_sync '&syncBrand.shortName.label;'> <!ENTITY fxaccount_status_sync '&syncBrand.shortName.label;'>
<!ENTITY fxaccount_status_sync_enabled '&syncBrand.shortName.label;: enabled'> <!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.'> <!ENTITY fxaccount_status_needs_verification2 'Your account needs to be verified. Tap to resend verification email.'>

Просмотреть файл

@ -272,8 +272,6 @@ gbjar.sources += [
'home/HomePagerTabStrip.java', 'home/HomePagerTabStrip.java',
'home/HomePanelPicker.java', 'home/HomePanelPicker.java',
'home/HomePanelsManager.java', 'home/HomePanelsManager.java',
'home/LastTabsPanel.java',
'home/MostRecentPanel.java',
'home/MultiTypeCursorAdapter.java', 'home/MultiTypeCursorAdapter.java',
'home/PanelAuthCache.java', 'home/PanelAuthCache.java',
'home/PanelAuthLayout.java', 'home/PanelAuthLayout.java',
@ -289,6 +287,7 @@ gbjar.sources += [
'home/PinSiteDialog.java', 'home/PinSiteDialog.java',
'home/ReadingListPanel.java', 'home/ReadingListPanel.java',
'home/ReadingListRow.java', 'home/ReadingListRow.java',
'home/RecentTabsPanel.java',
'home/SearchEngine.java', 'home/SearchEngine.java',
'home/SearchEngineRow.java', 'home/SearchEngineRow.java',
'home/SearchLoader.java', 'home/SearchLoader.java',

Просмотреть файл

@ -4,15 +4,21 @@
package org.mozilla.gecko.preferences; package org.mozilla.gecko.preferences;
import java.nio.ByteBuffer;
import java.text.Collator; import java.text.Collator;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.preference.ListPreference; import android.preference.ListPreference;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
@ -21,7 +27,51 @@ import android.util.Log;
public class LocaleListPreference extends ListPreference { public class LocaleListPreference extends ListPreference {
private static final String LOG_TAG = "GeckoLocaleList"; private static final String LOG_TAG = "GeckoLocaleList";
/**
* With thanks to <http://stackoverflow.com/a/22679283/22003> for the
* initial solution.
*
* This class encapsulates an approach to checking whether a script
* is usable on a device. We attempt to draw a character from the
* script (e.g., ). If the fonts on the device don't have the correct
* glyph, Android typically renders whitespace (rather than .notdef).
*
* Pass in part of the name of the locale in its local representation,
* and a whitespace character; this class performs the graphical comparison.
*
* See Bug 1023451 Comment 24 for extensive explanation.
*/
private static class CharacterValidator {
private static final int BITMAP_WIDTH = 32;
private static final int BITMAP_HEIGHT = 48;
private final Paint paint = new Paint();
private final byte[] missingCharacter;
public CharacterValidator(String missing) {
this.missingCharacter = getPixels(drawBitmap(missing));
}
private Bitmap drawBitmap(String text){
Bitmap b = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ALPHA_8);
Canvas c = new Canvas(b);
c.drawText(text, 0, BITMAP_HEIGHT / 2, this.paint);
return b;
}
private static byte[] getPixels(Bitmap b) {
ByteBuffer buffer = ByteBuffer.allocate(b.getByteCount());
b.copyPixelsToBuffer(buffer);
return buffer.array();
}
public boolean characterIsMissingInFont(String ch) {
byte[] rendered = getPixels(drawBitmap(ch));
return Arrays.equals(rendered, missingCharacter);
}
}
private volatile Locale entriesLocale; private volatile Locale entriesLocale;
private final CharacterValidator characterValidator;
public LocaleListPreference(Context context) { public LocaleListPreference(Context context) {
this(context, null); this(context, null);
@ -29,6 +79,10 @@ public class LocaleListPreference extends ListPreference {
public LocaleListPreference(Context context, AttributeSet attributes) { public LocaleListPreference(Context context, AttributeSet attributes) {
super(context, attributes); super(context, attributes);
// Thus far, missing glyphs are replaced by whitespace, not a box
// or other Unicode codepoint.
this.characterValidator = new CharacterValidator(" ");
buildList(); buildList();
} }
@ -86,9 +140,54 @@ public class LocaleListPreference extends ListPreference {
// We sort by name, so we use Collator. // We sort by name, so we use Collator.
return COLLATOR.compare(this.nativeName, another.nativeName); return COLLATOR.compare(this.nativeName, another.nativeName);
} }
/**
* See Bug 1023451 Comment 10 for the research that led to
* this method.
*
* @return true if this locale can be used for displaying UI
* on this device without known issues.
*/
public boolean isUsable(CharacterValidator validator) {
// Oh, for Java 7 switch statements.
if (this.tag.equals("bn-IN")) {
// Bengali sometimes has an English label if the Bengali script
// is missing. This prevents us from simply checking character
// rendering for bn-IN; we'll get a false positive for "B", not "".
//
// This doesn't seem to affect other Bengali-script locales
// (below), which always have a label in native script.
if (!this.nativeName.startsWith("বাংলা")) {
// We're on an Android version that doesn't even have
// characters to say . Definite failure.
return false;
}
} }
private LocaleDescriptor[] getShippingLocales() { // These locales use a script that is often unavailable
// on common Android devices. Make sure we can show them.
// See documentation for CharacterValidator.
// Note that bn-IN is checked here even if it passed above.
if (this.tag.equals("or") ||
this.tag.equals("pa-IN") ||
this.tag.equals("gu-IN") ||
this.tag.equals("bn-IN")) {
if (validator.characterIsMissingInFont(this.nativeName.substring(0, 1))) {
return false;
}
}
return true;
}
}
/**
* Not every locale we ship can be used on every device, due to
* font or rendering constraints.
*
* This method filters down the list before generating the descriptor array.
*/
private LocaleDescriptor[] getUsableLocales() {
Collection<String> shippingLocales = BrowserLocaleManager.getPackagedLocaleTags(getContext()); Collection<String> shippingLocales = BrowserLocaleManager.getPackagedLocaleTags(getContext());
// Future: single-locale builds should be specified, too. // Future: single-locale builds should be specified, too.
@ -97,15 +196,22 @@ public class LocaleListPreference extends ListPreference {
return new LocaleDescriptor[] { new LocaleDescriptor(fallbackTag) }; return new LocaleDescriptor[] { new LocaleDescriptor(fallbackTag) };
} }
final int count = shippingLocales.size(); final int initialCount = shippingLocales.size();
final LocaleDescriptor[] descriptors = new LocaleDescriptor[count]; final Set<LocaleDescriptor> locales = new HashSet<LocaleDescriptor>(initialCount);
int i = 0;
for (String tag : shippingLocales) { for (String tag : shippingLocales) {
descriptors[i++] = new LocaleDescriptor(tag); final LocaleDescriptor descriptor = new LocaleDescriptor(tag);
if (!descriptor.isUsable(this.characterValidator)) {
Log.w(LOG_TAG, "Skipping locale " + tag + " on this device.");
continue;
} }
Arrays.sort(descriptors, 0, count); locales.add(descriptor);
}
final int usableCount = locales.size();
final LocaleDescriptor[] descriptors = locales.toArray(new LocaleDescriptor[usableCount]);
Arrays.sort(descriptors, 0, usableCount);
return descriptors; return descriptors;
} }
@ -154,7 +260,7 @@ public class LocaleListPreference extends ListPreference {
return; return;
} }
final LocaleDescriptor[] descriptors = getShippingLocales(); final LocaleDescriptor[] descriptors = getUsableLocales();
final int count = descriptors.length; final int count = descriptors.length;
this.entriesLocale = currentLocale; this.entriesLocale = currentLocale;

Просмотреть файл

@ -1,48 +0,0 @@
<?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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="false"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@android:color/transparent"/>
<item android:state_focused="false"
android:state_selected="true"
android:state_pressed="false"
android:drawable="@color/background_light"/>
<item android:state_focused="true"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="true"
android:state_pressed="false"
android:drawable="@color/background_light"/>
<item android:state_focused="false"
android:state_selected="false"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="false"
android:state_selected="true"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="false"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="true"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
</selector>

Просмотреть файл

@ -1,48 +0,0 @@
<?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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="false"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@android:color/transparent"/>
<item android:state_focused="false"
android:state_selected="true"
android:state_pressed="false"
android:drawable="@color/background_light"/>
<item android:state_focused="true"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="true"
android:state_pressed="false"
android:drawable="@color/background_light"/>
<item android:state_focused="false"
android:state_selected="false"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="false"
android:state_selected="true"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="false"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="true"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
</selector>

Просмотреть файл

@ -1,48 +0,0 @@
<?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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="false"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@android:color/transparent"/>
<item android:state_focused="false"
android:state_selected="true"
android:state_pressed="false"
android:drawable="@drawable/history_tabs_indicator_selected"/>
<item android:state_focused="true"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="true"
android:state_pressed="false"
android:drawable="@drawable/history_tabs_indicator_selected"/>
<item android:state_focused="false"
android:state_selected="false"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="false"
android:state_selected="true"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="false"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
<item android:state_focused="true"
android:state_selected="true"
android:state_pressed="true"
android:drawable="@color/background_normal"/>
</selector>

Просмотреть файл

@ -1,24 +0,0 @@
<?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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.mozilla.gecko.widget.IconTabWidget android:id="@+id/tab_icon_widget"
style="@style/Widget.Home.HistoryTabWidget"
android:layout_width="@dimen/history_tab_widget_width"
android:layout_height="@dimen/history_tab_widget_height"
android:orientation="vertical"
android:layout="@layout/home_history_tabs_indicator"
gecko:display="text"/>
<FrameLayout android:id="@+id/history_panel_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>

Просмотреть файл

@ -1,10 +0,0 @@
<?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/. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.Home.HistoryTabIndicator"
android:layout_width="match_parent"
android:layout_height="@dimen/history_tab_indicator_height"
android:background="@drawable/home_history_tabs_indicator"/>

Просмотреть файл

@ -1,24 +0,0 @@
<?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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.mozilla.gecko.widget.IconTabWidget android:id="@+id/tab_icon_widget"
style="@style/Widget.Home.HistoryTabWidget"
android:layout_width="@dimen/history_tab_widget_width"
android:layout_height="@dimen/history_tab_widget_height"
android:orientation="vertical"
android:layout="@layout/home_history_tabs_indicator"
gecko:display="text"/>
<FrameLayout android:id="@+id/history_panel_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>

Просмотреть файл

@ -1,10 +0,0 @@
<?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/. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.Home.HistoryTabIndicator"
android:layout_width="match_parent"
android:layout_height="@dimen/history_tab_indicator_height"
android:background="@drawable/home_history_tabs_indicator"/>

Просмотреть файл

@ -5,8 +5,8 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. - file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:fillViewport="true" > android:fillViewport="true" >
<LinearLayout style="@style/FxAccountMiddle" > <LinearLayout style="@style/FxAccountMiddle" >

Просмотреть файл

@ -5,8 +5,8 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. - file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:fillViewport="true" > android:fillViewport="true" >
<LinearLayout style="@style/FxAccountMiddle" > <LinearLayout style="@style/FxAccountMiddle" >

Просмотреть файл

@ -6,8 +6,8 @@
--> -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:fillViewport="true" > android:fillViewport="true" >
<LinearLayout <LinearLayout
@ -20,6 +20,8 @@
style="@style/FxAccountHeaderItem" style="@style/FxAccountHeaderItem"
android:text="@string/fxaccount_create_account_header" /> android:text="@string/fxaccount_create_account_header" />
<include layout="@layout/fxaccount_custom_server_view" />
<include layout="@layout/fxaccount_email_password_view" /> <include layout="@layout/fxaccount_email_password_view" />
<TextView <TextView

Просмотреть файл

@ -5,8 +5,8 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. - file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:fillViewport="true" > android:fillViewport="true" >
<LinearLayout <LinearLayout

Просмотреть файл

@ -0,0 +1,116 @@
<?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/.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<LinearLayout
android:id="@+id/account_server_layout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:background="@color/fxaccount_error_preference_backgroundcolor"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/fxaccount_empty_contentDescription"
android:gravity="center"
android:minWidth="48dip"
android:padding="10dip"
android:src="@drawable/fxaccount_sync_error" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingBottom="6dip"
android:paddingRight="10dip"
android:paddingTop="6dip" >
<TextView
android:id="@+android:id/account_server_title"
style="@style/FxAccountTextItem"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="0dp"
android:gravity="center_vertical"
android:text="@string/fxaccount_custom_server_account_title" >
</TextView>
<TextView
android:id="@+android:id/account_server_summary"
style="@style/FxAccountTextItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:ellipsize="middle"
android:focusable="true"
android:focusableInTouchMode="true"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/sync_server_layout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:background="@color/fxaccount_error_preference_backgroundcolor"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/fxaccount_empty_contentDescription"
android:gravity="center"
android:minWidth="48dip"
android:padding="10dip"
android:src="@drawable/fxaccount_sync_error" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingBottom="6dip"
android:paddingRight="10dip"
android:paddingTop="6dip" >
<TextView
android:id="@+android:id/sync_server_title"
style="@style/FxAccountTextItem"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="0dp"
android:gravity="center_vertical"
android:text="@string/fxaccount_custom_server_sync_title" >
</TextView>
<TextView
android:id="@+android:id/sync_server_summary"
style="@style/FxAccountTextItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:ellipsize="marquee"
android:focusable="true"
android:focusableInTouchMode="true"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</merge>

Просмотреть файл

@ -8,7 +8,7 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android" > <merge xmlns:android="http://schemas.android.com/apk/res/android" >
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" > android:orientation="vertical" >
@ -25,14 +25,14 @@
</AutoCompleteTextView> </AutoCompleteTextView>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" > android:orientation="horizontal" >
<EditText <EditText
android:id="@+id/password" android:id="@+id/password"
style="@style/FxAccountEditItem" style="@style/FxAccountEditItem"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/fxaccount_password_background" android:background="@drawable/fxaccount_password_background"
@ -51,22 +51,22 @@
happy. Be thankful there are not three buttons! --> happy. Be thankful there are not three buttons! -->
<FrameLayout <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:layout_weight="0" android:layout_weight="0"
android:orientation="horizontal" > android:orientation="horizontal" >
<Button <Button
android:id="@+id/show_password" android:id="@+id/show_password"
style="@style/FxAccountShowHidePasswordButton" style="@style/FxAccountShowHidePasswordButton"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:text="@string/fxaccount_password_show" > android:text="@string/fxaccount_password_show" >
</Button> </Button>
<Button <Button
style="@style/FxAccountShowHidePasswordButton" style="@style/FxAccountShowHidePasswordButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:text="@string/fxaccount_password_show" android:text="@string/fxaccount_password_show"
android:visibility="invisible" > android:visibility="invisible" >
</Button> </Button>
@ -74,7 +74,7 @@
<Button <Button
style="@style/FxAccountShowHidePasswordButton" style="@style/FxAccountShowHidePasswordButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:text="@string/fxaccount_password_hide" android:text="@string/fxaccount_password_hide"
android:visibility="invisible" > android:visibility="invisible" >
</Button> </Button>

Просмотреть файл

@ -5,8 +5,8 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. - file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:fillViewport="true" > android:fillViewport="true" >
<LinearLayout <LinearLayout

Просмотреть файл

@ -19,12 +19,12 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:background="@android:color/transparent"> android:background="@android:color/transparent">
<ListView android:id="@android:id/list" <ListView android:id="@android:id/list"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="0px" android:layout_height="0px"
android:layout_weight="1" android:layout_weight="1"
android:paddingTop="0dip" android:paddingTop="0dip"

Просмотреть файл

@ -6,8 +6,8 @@
--> -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:fillViewport="true" > android:fillViewport="true" >
<LinearLayout <LinearLayout
@ -20,6 +20,8 @@
style="@style/FxAccountHeaderItem" style="@style/FxAccountHeaderItem"
android:text="@string/fxaccount_sign_in_sub_header" /> android:text="@string/fxaccount_sign_in_sub_header" />
<include layout="@layout/fxaccount_custom_server_view" />
<include layout="@layout/fxaccount_email_password_view" /> <include layout="@layout/fxaccount_email_password_view" />
<TextView <TextView
@ -39,7 +41,7 @@
</RelativeLayout> </RelativeLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:orientation="horizontal" > android:orientation="horizontal" >

Просмотреть файл

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/fxaccount_error_preference_backgroundcolor" android:background="@color/fxaccount_error_preference_backgroundcolor"
android:gravity="center_vertical" android:gravity="center_vertical"
@ -35,7 +35,7 @@
<TextView <TextView
android:id="@+android:id/title" android:id="@+android:id/title"
style="@style/FxAccountTextItem" style="@style/FxAccountTextItem"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:gravity="center_vertical" > android:gravity="center_vertical" >

Просмотреть файл

@ -6,8 +6,8 @@
--> -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:fillViewport="true" > android:fillViewport="true" >
<LinearLayout <LinearLayout
@ -20,6 +20,8 @@
style="@style/FxAccountHeaderItem" style="@style/FxAccountHeaderItem"
android:text="@string/fxaccount_update_credentials_header" /> android:text="@string/fxaccount_update_credentials_header" />
<include layout="@layout/fxaccount_custom_server_view" />
<include layout="@layout/fxaccount_email_password_view" /> <include layout="@layout/fxaccount_email_password_view" />
<TextView <TextView
@ -41,6 +43,7 @@
<TextView <TextView
android:id="@+id/forgot_password_link" android:id="@+id/forgot_password_link"
style="@style/FxAccountLinkifiedItem" style="@style/FxAccountLinkifiedItem"
android:layout_marginTop="10dp"
android:text="@string/fxaccount_sign_in_forgot_password" /> android:text="@string/fxaccount_sign_in_forgot_password" />
<LinearLayout style="@style/FxAccountSpacer" /> <LinearLayout style="@style/FxAccountSpacer" />

Просмотреть файл

@ -8,17 +8,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout android:id="@+id/history_panel_container" <include layout="@layout/home_history_list"/>
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<org.mozilla.gecko.widget.IconTabWidget android:id="@+id/tab_icon_widget" <LinearLayout android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="@dimen/browser_toolbar_height" android:background="@color/home_button_bar_bg">
android:tabStripEnabled="false"
android:showDividers="none" <Button android:id="@+id/clear_history_button"
android:background="@color/background_light" style="@style/Widget.Home.ActionButton"
android:layout="@layout/home_history_tabs_indicator"/> android:text="@string/home_clear_history_button"
android:gravity="center"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

Просмотреть файл

@ -1,10 +0,0 @@
<?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/. -->
<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/home_history_tabs_indicator"/>

Просмотреть файл

@ -1,25 +0,0 @@
<?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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/home_history_list"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/home_button_bar_bg">
<Button android:id="@+id/clear_history_button"
style="@style/Widget.Home.ActionButton"
android:text="@string/home_clear_history_button"
android:gravity="center"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>

Просмотреть файл

@ -10,16 +10,4 @@
<include layout="@layout/home_history_list"/> <include layout="@layout/home_history_list"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/home_button_bar_bg">
<Button android:id="@+id/open_all_tabs_button"
style="@style/Widget.Home.ActionButton"
android:text="@string/home_last_tabs_open"
android:gravity="center"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

Просмотреть файл

@ -5,7 +5,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:gravity="center_vertical" android:gravity="center_vertical"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight" android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="horizontal" android:orientation="horizontal"

Просмотреть файл

@ -11,20 +11,20 @@
android:text="@string/sync_title_send_tab" /> android:text="@string/sync_title_send_tab" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/SyncSpace" > android:padding="@dimen/SyncSpace" >
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/sync_title_send_tab" /> android:text="@string/sync_title_send_tab" />
<TextView <TextView
android:id="@+id/uri" android:id="@+id/uri"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/sync_title_send_tab" /> android:text="@string/sync_title_send_tab" />

Просмотреть файл

@ -19,7 +19,7 @@
<ProgressBar <ProgressBar
android:id="@+id/waiting_content1" android:id="@+id/waiting_content1"
style="@style/Widget.ProgressBar.Horizontal" style="@style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminateOnly="true" android:indeterminateOnly="true"
android:padding="@dimen/SyncSpace" /> android:padding="@dimen/SyncSpace" />

Просмотреть файл

@ -6,6 +6,6 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/SyncLayout" > style="@style/SyncLayout" >
<WebView android:id="@+id/web_engine" <WebView android:id="@+id/web_engine"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" /> android:layout_height="fill_parent" />
</LinearLayout> </LinearLayout>

Просмотреть файл

@ -1,10 +0,0 @@
<?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>
<dimen name="history_tab_widget_width">360dp</dimen>
</resources>

Просмотреть файл

@ -7,7 +7,7 @@
<!-- Top title bar: a text view with the Sync icon to the left. --> <!-- Top title bar: a text view with the Sync icon to the left. -->
<style name="SyncTop" parent="@android:style/Widget.Holo.ActionBar"> <style name="SyncTop" parent="@android:style/Widget.Holo.ActionBar">
<item name="android:textAppearance">@android:style/TextAppearance.Holo.Widget.ActionBar.Title</item> <item name="android:textAppearance">@android:style/TextAppearance.Holo.Widget.ActionBar.Title</item>
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:gravity">center_vertical|left</item> <item name="android:gravity">center_vertical|left</item>
<item name="android:drawableLeft">@drawable/icon</item> <item name="android:drawableLeft">@drawable/icon</item>
@ -17,12 +17,12 @@
<!-- Bottom bar: a horizontal linear layout with buttons in it. --> <!-- Bottom bar: a horizontal linear layout with buttons in it. -->
<style name="SyncBottom" parent="@android:style/Holo.Light.ButtonBar"> <style name="SyncBottom" parent="@android:style/Holo.Light.ButtonBar">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
</style> </style>
<style name="SyncButton" parent="@android:style/Widget.Holo.Light.Button.Small"> <style name="SyncButton" parent="@android:style/Widget.Holo.Light.Button.Small">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item> <item name="android:layout_weight">1</item>
<item name="android:background">?android:attr/selectableItemBackground</item> <item name="android:background">?android:attr/selectableItemBackground</item>

Просмотреть файл

@ -1,10 +0,0 @@
<?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>
<dimen name="history_tab_widget_width">480dp</dimen>
</resources>

Просмотреть файл

@ -11,7 +11,6 @@
<dimen name="tabs_counter_size">26sp</dimen> <dimen name="tabs_counter_size">26sp</dimen>
<dimen name="tabs_panel_indicator_width">60dp</dimen> <dimen name="tabs_panel_indicator_width">60dp</dimen>
<dimen name="tabs_panel_list_padding">8dip</dimen> <dimen name="tabs_panel_list_padding">8dip</dimen>
<dimen name="history_tab_widget_width">270dp</dimen>
<dimen name="panel_grid_view_column_width">250dp</dimen> <dimen name="panel_grid_view_column_width">250dp</dimen>
</resources> </resources>

Просмотреть файл

@ -82,12 +82,6 @@
<dimen name="url_bar_offset_left">32dp</dimen> <dimen name="url_bar_offset_left">32dp</dimen>
<dimen name="history_tab_indicator_height">50dp</dimen> <dimen name="history_tab_indicator_height">50dp</dimen>
<!-- We need to maintain height for the tab widget on History Page
since android does not add footer/header divider height to its
calculation for wrap_content in LinearLayout.
50dp * 2 Views + 30dp padding + 4dp dividers-->
<dimen name="history_tab_widget_height">134dp</dimen>
<!-- PageActionButtons dimensions --> <!-- PageActionButtons dimensions -->
<dimen name="page_action_button_width">32dp</dimen> <dimen name="page_action_button_width">32dp</dimen>

Просмотреть файл

@ -16,7 +16,7 @@
<style name="FxAccountMiddle"> <style name="FxAccountMiddle">
<item name="android:background">@android:color/white</item> <item name="android:background">@android:color/white</item>
<item name="android:orientation">vertical</item> <item name="android:orientation">vertical</item>
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item> <item name="android:layout_weight">1</item>
<item name="android:paddingTop">30dp</item> <item name="android:paddingTop">30dp</item>
@ -27,7 +27,7 @@
<style name="FxAccountSpacer"> <style name="FxAccountSpacer">
<item name="android:orientation">vertical</item> <item name="android:orientation">vertical</item>
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">0dp</item> <item name="android:layout_height">0dp</item>
<item name="android:layout_weight">1</item> <item name="android:layout_weight">1</item>
</style> </style>
@ -39,7 +39,7 @@
<style name="FxAccountTextItem" parent="@android:style/TextAppearance.Medium"> <style name="FxAccountTextItem" parent="@android:style/TextAppearance.Medium">
<item name="android:textColor">@color/fxaccount_textColor</item> <item name="android:textColor">@color/fxaccount_textColor</item>
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:gravity">center_horizontal</item> <item name="android:gravity">center_horizontal</item>
<item name="android:textSize">16sp</item> <item name="android:textSize">16sp</item>
@ -57,7 +57,7 @@
<item name="android:textColor">@drawable/fxaccount_button_color</item> <item name="android:textColor">@drawable/fxaccount_button_color</item>
<item name="android:textSize">24sp</item> <item name="android:textSize">24sp</item>
<item name="android:padding">20dp</item> <item name="android:padding">20dp</item>
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginBottom">10dp</item> <item name="android:layout_marginBottom">10dp</item>
</style> </style>
@ -81,7 +81,7 @@
<item name="android:focusable">true</item> <item name="android:focusable">true</item>
<item name="android:textColor">@color/fxaccount_linkified_textColor</item> <item name="android:textColor">@color/fxaccount_linkified_textColor</item>
<item name="android:textColorLink">@color/fxaccount_linkified_textColorLink</item> <item name="android:textColorLink">@color/fxaccount_linkified_textColorLink</item>
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
</style> </style>
@ -95,7 +95,7 @@
</style> </style>
<style name="FxAccountErrorItem"> <style name="FxAccountErrorItem">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_marginBottom">10dp</item> <item name="android:layout_marginBottom">10dp</item>
<item name="android:layout_marginTop">10dp</item> <item name="android:layout_marginTop">10dp</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
@ -118,13 +118,13 @@
<style name="FxAccountButtonLayout"> <style name="FxAccountButtonLayout">
<item name="android:orientation">vertical</item> <item name="android:orientation">vertical</item>
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:background">@drawable/fxaccount_button_background</item> <item name="android:background">@drawable/fxaccount_button_background</item>
</style> </style>
<style name="FxAccountCheckBox"> <style name="FxAccountCheckBox">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginBottom">10dp</item> <item name="android:layout_marginBottom">10dp</item>
<item name="android:textColor">@drawable/fxaccount_checkbox_textcolor</item> <item name="android:textColor">@drawable/fxaccount_checkbox_textcolor</item>

Просмотреть файл

@ -5,8 +5,8 @@
<resources> <resources>
<style name="SyncLayout"> <style name="SyncLayout">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">match_parent</item> <item name="android:layout_height">fill_parent</item>
</style> </style>
<style name="SyncLayout.Vertical" parent="@style/SyncLayout"> <style name="SyncLayout.Vertical" parent="@style/SyncLayout">
<item name="android:orientation">vertical</item> <item name="android:orientation">vertical</item>
@ -17,14 +17,14 @@
<!-- TextView Styles --> <!-- TextView Styles -->
<style name="SyncTextFrame" parent="@style/TextAppearance"> <style name="SyncTextFrame" parent="@style/TextAppearance">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">match_parent</item> <item name="android:layout_height">fill_parent</item>
<item name="android:layout_gravity">center</item> <item name="android:layout_gravity">center</item>
<item name="android:padding">@dimen/SyncSpace</item> <item name="android:padding">@dimen/SyncSpace</item>
<item name="android:orientation">vertical</item> <item name="android:orientation">vertical</item>
</style> </style>
<style name="SyncTextItem" parent="@style/TextAppearance.Medium"> <style name="SyncTextItem" parent="@style/TextAppearance.Medium">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:textSize">15dp</item> <item name="android:textSize">15dp</item>
</style> </style>
@ -44,7 +44,7 @@
</style> </style>
<!-- EditView Styles --> <!-- EditView Styles -->
<style name="SyncEditItem" parent="@style/Widget.EditText"> <style name="SyncEditItem" parent="@style/Widget.EditText">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
<item name="android:inputType">text|textNoSuggestions</item> <item name="android:inputType">text|textNoSuggestions</item>
@ -67,7 +67,7 @@
<!-- Top title bar: a text view with the Sync icon to the left. --> <!-- Top title bar: a text view with the Sync icon to the left. -->
<style name="SyncTop"> <style name="SyncTop">
<item name="android:textAppearance">@style/TextAppearance.Large</item> <item name="android:textAppearance">@style/TextAppearance.Large</item>
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:gravity">center_vertical|left</item> <item name="android:gravity">center_vertical|left</item>
<item name="android:drawableLeft">@drawable/icon</item> <item name="android:drawableLeft">@drawable/icon</item>
@ -80,7 +80,7 @@
<!-- Middle scroller: a scroll view with content in it. --> <!-- Middle scroller: a scroll view with content in it. -->
<style name="SyncMiddle"> <style name="SyncMiddle">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">0dp</item> <item name="android:layout_height">0dp</item>
<item name="android:layout_weight">1</item> <item name="android:layout_weight">1</item>
<item name="android:padding">@dimen/SyncSpace</item> <item name="android:padding">@dimen/SyncSpace</item>
@ -88,7 +88,7 @@
<!-- Bottom bar: a horizontal linear layout with buttons in it. --> <!-- Bottom bar: a horizontal linear layout with buttons in it. -->
<style name="SyncBottom"> <style name="SyncBottom">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center</item> <item name="android:layout_gravity">center</item>
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
@ -104,7 +104,7 @@
</style> </style>
<style name="SyncButton" parent="@style/Widget.Button"> <style name="SyncButton" parent="@style/Widget.Button">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item> <item name="android:layout_weight">1</item>
</style> </style>

Просмотреть файл

@ -10,6 +10,11 @@
android:key="email" android:key="email"
android:persistent="false" android:persistent="false"
android:title="@string/fxaccount_email_hint" /> android:title="@string/fxaccount_email_hint" />
<Preference
android:editable="false"
android:key="auth_server"
android:persistent="false"
android:title="@string/fxaccount_status_auth_server" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="sync_category" android:key="sync_category"
@ -72,6 +77,13 @@
android:key="device_name" android:key="device_name"
android:persistent="false" android:persistent="false"
android:title="@string/fxaccount_status_device_name" /> android:title="@string/fxaccount_status_device_name" />
<Preference
android:editable="false"
android:key="sync_server"
android:persistent="false"
android:title="@string/fxaccount_status_sync_server" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="legal_category" android:key="legal_category"

Просмотреть файл

@ -36,6 +36,7 @@
<string name="bookmarks_title">&bookmarks_title;</string> <string name="bookmarks_title">&bookmarks_title;</string>
<string name="history_title">&history_title;</string> <string name="history_title">&history_title;</string>
<string name="reading_list_title">&reading_list_title;</string> <string name="reading_list_title">&reading_list_title;</string>
<string name="recent_tabs_title">&recent_tabs_title;</string>
<string name="switch_to_tab">&switch_to_tab;</string> <string name="switch_to_tab">&switch_to_tab;</string>
@ -313,10 +314,10 @@
<string name="home_clear_history_button">&home_clear_history_button;</string> <string name="home_clear_history_button">&home_clear_history_button;</string>
<string name="home_clear_history_confirm">&home_clear_history_confirm;</string> <string name="home_clear_history_confirm">&home_clear_history_confirm;</string>
<string name="home_bookmarks_empty">&home_bookmarks_empty;</string> <string name="home_bookmarks_empty">&home_bookmarks_empty;</string>
<string name="home_closed_tabs_title">&home_closed_tabs_title;</string>
<string name="home_last_tabs_title">&home_last_tabs_title;</string> <string name="home_last_tabs_title">&home_last_tabs_title;</string>
<string name="home_last_tabs_open">&home_last_tabs_open;</string> <string name="home_last_tabs_open">&home_last_tabs_open;</string>
<string name="home_last_tabs_empty">&home_last_tabs_empty;</string> <string name="home_last_tabs_empty">&home_last_tabs_empty;</string>
<string name="home_most_recent_title">&home_most_recent_title;</string>
<string name="home_most_recent_empty">&home_most_recent_empty;</string> <string name="home_most_recent_empty">&home_most_recent_empty;</string>
<string name="home_reading_list_empty">&home_reading_list_empty;</string> <string name="home_reading_list_empty">&home_reading_list_empty;</string>
<string name="home_reading_list_hint">&home_reading_list_hint2;</string> <string name="home_reading_list_hint">&home_reading_list_hint2;</string>

Просмотреть файл

@ -472,7 +472,7 @@ public class SyncConfiguration {
} else { } else {
edit.putString(PREF_ENABLED_ENGINE_NAMES, setToJSONObjectString(enabledEngineNames)); edit.putString(PREF_ENABLED_ENGINE_NAMES, setToJSONObjectString(enabledEngineNames));
} }
if (declinedEngineNames.isEmpty()) { if (declinedEngineNames == null || declinedEngineNames.isEmpty()) {
edit.remove(PREF_DECLINED_ENGINE_NAMES); edit.remove(PREF_DECLINED_ENGINE_NAMES);
} else { } else {
edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declinedEngineNames)); edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declinedEngineNames));

Просмотреть файл

@ -26,7 +26,14 @@ import com.jayway.android.robotium.solo.Condition;
* To use any of these methods in your test make sure it extends AboutHomeTest instead of BaseTest * To use any of these methods in your test make sure it extends AboutHomeTest instead of BaseTest
*/ */
abstract class AboutHomeTest extends PixelTest { abstract class AboutHomeTest extends PixelTest {
protected enum AboutHomeTabs {HISTORY, MOST_RECENT, TABS_FROM_LAST_TIME, TOP_SITES, BOOKMARKS, READING_LIST}; protected enum AboutHomeTabs {
RECENT_TABS,
HISTORY,
TOP_SITES,
BOOKMARKS,
READING_LIST
};
private ArrayList<String> aboutHomeTabs = new ArrayList<String>() {{ private ArrayList<String> aboutHomeTabs = new ArrayList<String>() {{
add("TOP_SITES"); add("TOP_SITES");
add("BOOKMARKS"); add("BOOKMARKS");
@ -42,8 +49,10 @@ abstract class AboutHomeTest extends PixelTest {
// Update it for tablets vs. phones. // Update it for tablets vs. phones.
if (mDevice.type.equals("phone")) { if (mDevice.type.equals("phone")) {
aboutHomeTabs.add(0, AboutHomeTabs.HISTORY.toString()); aboutHomeTabs.add(0, AboutHomeTabs.HISTORY.toString());
aboutHomeTabs.add(0, AboutHomeTabs.RECENT_TABS.toString());
} else { } else {
aboutHomeTabs.add(AboutHomeTabs.HISTORY.toString()); aboutHomeTabs.add(AboutHomeTabs.HISTORY.toString());
aboutHomeTabs.add(AboutHomeTabs.RECENT_TABS.toString());
} }
} }
} }
@ -223,7 +232,7 @@ abstract class AboutHomeTest extends PixelTest {
/** /**
* This method can be used to open the different tabs of about:home. * This method can be used to open the different tabs of about:home.
* *
* @param AboutHomeTabs enum item {MOST_RECENT, TABS_FROM_LAST_TIME, TOP_SITES, BOOKMARKS, READING_LIST} * @param AboutHomeTabs enum item
*/ */
protected void openAboutHomeTab(AboutHomeTabs tab) { protected void openAboutHomeTab(AboutHomeTabs tab) {
focusUrlBar(); focusUrlBar();
@ -233,71 +242,13 @@ abstract class AboutHomeTest extends PixelTest {
// Handle tablets by just clicking the visible tab title. // Handle tablets by just clicking the visible tab title.
if (mDevice.type.equals("tablet")) { if (mDevice.type.equals("tablet")) {
if (AboutHomeTabs.MOST_RECENT == tab || AboutHomeTabs.TABS_FROM_LAST_TIME == tab) {
tabOffset = aboutHomeTabs.indexOf(AboutHomeTabs.HISTORY.toString()) - currentTabIndex;
swipeAboutHome(tabOffset);
waitForAboutHomeTab(aboutHomeTabs.indexOf(StringHelper.HISTORY_LABEL));
TabWidget tabwidget = (TabWidget)mSolo.getView(TabWidget.class, 0);
switch (tab) {
case MOST_RECENT: {
mSolo.clickOnView(tabwidget.getChildAt(0));
// We can determine if we are on the MOST_RECENT tab only if pages were first visited during the test
mAsserter.ok(waitForText(StringHelper.TODAY_LABEL), "Checking that we are in the most recent tab of about:home", "We are in the most recent tab");
break;
}
case TABS_FROM_LAST_TIME: {
mSolo.clickOnView(tabwidget.getChildAt(1));
mAsserter.ok(waitForText(StringHelper.TABS_FROM_LAST_TIME_LABEL), "Checking that we are in the Tabs from last time tab of about:home", "We are in the Tabs from last time tab");
break;
}
}
} else {
clickAboutHomeTab(tab); clickAboutHomeTab(tab);
}
return; return;
} }
// Handle phones (non-tablets). // Handle phones (non-tablets).
tabOffset = aboutHomeTabs.indexOf(tab.toString()) - currentTabIndex; tabOffset = aboutHomeTabs.indexOf(tab.toString()) - currentTabIndex;
switch (tab) {
case TOP_SITES : {
swipeAboutHome(tabOffset); swipeAboutHome(tabOffset);
waitForAboutHomeTab(aboutHomeTabs.indexOf(tab.toString())); waitForAboutHomeTab(aboutHomeTabs.indexOf(tab.toString()));
break;
}
case BOOKMARKS : {
swipeAboutHome(tabOffset);
waitForAboutHomeTab(aboutHomeTabs.indexOf(tab.toString()));
break;
}
case MOST_RECENT: {
// MOST_RECENT is contained in the HISTORY tab.
tabOffset = aboutHomeTabs.indexOf(AboutHomeTabs.HISTORY.toString()) - currentTabIndex;
swipeAboutHome(tabOffset);
waitForAboutHomeTab(aboutHomeTabs.indexOf(StringHelper.HISTORY_LABEL));
TabWidget tabwidget = (TabWidget)mSolo.getView(TabWidget.class, 0);
mSolo.clickOnView(tabwidget.getChildAt(0));
// We can determine if we are on the MOST_RECENT tab only if pages were first visited during the test
mAsserter.ok(waitForText(StringHelper.TODAY_LABEL), "Checking that we are in the most recent tab of about:home", "We are in the most recent tab");
break;
}
case TABS_FROM_LAST_TIME: {
// TABS_FROM_LAST_TIME is contained in the HISTORY tab.
tabOffset = aboutHomeTabs.indexOf(AboutHomeTabs.HISTORY.toString()) - currentTabIndex;
swipeAboutHome(tabOffset);
waitForAboutHomeTab(aboutHomeTabs.indexOf(StringHelper.HISTORY_LABEL));
TabWidget tabwidget = (TabWidget)mSolo.getView(TabWidget.class, 0);
mSolo.clickOnView(tabwidget.getChildAt(1));
mAsserter.ok(waitForText(StringHelper.TABS_FROM_LAST_TIME_LABEL), "Checking that we are in the Tabs from last time tab of about:home", "We are in the Tabs from last time tab");
break;
}
case READING_LIST: {
swipeAboutHome(tabOffset);
waitForAboutHomeTab(aboutHomeTabs.indexOf(tab.toString()));
break;
}
}
} }
} }

Просмотреть файл

@ -30,13 +30,15 @@ public class AboutHomeComponent extends BaseComponent {
HISTORY, HISTORY,
TOP_SITES, TOP_SITES,
BOOKMARKS, BOOKMARKS,
READING_LIST READING_LIST,
RECENT_TABS
} }
// TODO: Having a specific ordering of panels is prone to fail and thus temporary. // TODO: Having a specific ordering of panels is prone to fail and thus temporary.
// Hopefully the work in bug 940565 will alleviate the need for these enums. // Hopefully the work in bug 940565 will alleviate the need for these enums.
// Explicit ordering of HomePager panels on a phone. // Explicit ordering of HomePager panels on a phone.
private enum PhonePanel { private enum PhonePanel {
RECENT_TABS,
HISTORY, HISTORY,
TOP_SITES, TOP_SITES,
BOOKMARKS, BOOKMARKS,
@ -48,7 +50,8 @@ public class AboutHomeComponent extends BaseComponent {
TOP_SITES, TOP_SITES,
BOOKMARKS, BOOKMARKS,
READING_LIST, READING_LIST,
HISTORY HISTORY,
RECENT_TABS
} }
// The percentage of the panel to swipe between 0 and 1. This value was set through // The percentage of the panel to swipe between 0 and 1. This value was set through

Просмотреть файл

@ -6,6 +6,8 @@ import org.mozilla.gecko.tests.helpers.GeckoHelper;
/** /**
* Tests functionality related to navigating between the various about:home panels. * Tests functionality related to navigating between the various about:home panels.
*
* TODO: Update this test to account for recent tabs panel (bug 1028727).
*/ */
public class testAboutHomePageNavigation extends UITest { public class testAboutHomePageNavigation extends UITest {
// TODO: Define this test dynamically by creating dynamic representations of the Page // TODO: Define this test dynamically by creating dynamic representations of the Page

Просмотреть файл

@ -21,9 +21,9 @@ public class testHistory extends AboutHomeTest {
inputAndLoadUrl(url3); inputAndLoadUrl(url3);
verifyPageTitle("Browser Blank Page 03"); verifyPageTitle("Browser Blank Page 03");
openAboutHomeTab(AboutHomeTabs.MOST_RECENT); openAboutHomeTab(AboutHomeTabs.HISTORY);
final ListView hList = findListViewWithTag("most_recent"); final ListView hList = findListViewWithTag("history");
mAsserter.is(waitForNonEmptyListToLoad(hList), true, "list is properly loaded"); mAsserter.is(waitForNonEmptyListToLoad(hList), true, "list is properly loaded");
// Click on the history item and wait for the page to load // Click on the history item and wait for the page to load

Просмотреть файл

@ -106,8 +106,8 @@ public class testReaderMode extends AboutHomeTest {
mAsserter.ok(mSolo.waitForText("Robocop Text Page"), "Verify if the page is added to your Reading List", "The page is present in your Reading List"); mAsserter.ok(mSolo.waitForText("Robocop Text Page"), "Verify if the page is added to your Reading List", "The page is present in your Reading List");
// Check if the page is added in History tab like a Reading List item // Check if the page is added in History tab like a Reading List item
openAboutHomeTab(AboutHomeTabs.MOST_RECENT); openAboutHomeTab(AboutHomeTabs.HISTORY);
list = findListViewWithTag("most_recent"); list = findListViewWithTag("history");
child = list.getChildAt(1); child = list.getChildAt(1);
mAsserter.ok(child != null, "item can be retrieved", child != null ? child.toString() : "null!"); mAsserter.ok(child != null, "item can be retrieved", child != null ? child.toString() : "null!");
mSolo.clickLongOnView(child); mSolo.clickLongOnView(child);

Просмотреть файл

@ -119,10 +119,10 @@ public class testShareLink extends AboutHomeTest {
mSolo.clickLongOnView(mostVisitedItem); mSolo.clickLongOnView(mostVisitedItem);
verifySharePopup(shareOptions,"top_sites"); verifySharePopup(shareOptions,"top_sites");
// Test the share popup in the Most Recent tab // Test the share popup in the history tab
openAboutHomeTab(AboutHomeTabs.MOST_RECENT); openAboutHomeTab(AboutHomeTabs.HISTORY);
ListView mostRecentList = findListViewWithTag("most_recent"); ListView mostRecentList = findListViewWithTag("history");
mAsserter.is(waitForNonEmptyListToLoad(mostRecentList), true, "list is properly loaded"); mAsserter.is(waitForNonEmptyListToLoad(mostRecentList), true, "list is properly loaded");
// Getting second child after header views because the first is the "Today" label // Getting second child after header views because the first is the "Today" label

Просмотреть файл

@ -43,13 +43,16 @@ SessionStore.prototype = {
_windows: {}, _windows: {},
_lastSaveTime: 0, _lastSaveTime: 0,
_interval: 10000, _interval: 10000,
_maxTabsUndo: 1, _maxTabsUndo: 5,
_pendingWrite: 0, _pendingWrite: 0,
// The index where the most recently closed tab was in the tabs array // The index where the most recently closed tab was in the tabs array
// when it was closed. // when it was closed.
_lastClosedTabIndex: -1, _lastClosedTabIndex: -1,
// Whether or not to send notifications for changes to the closed tabs.
_notifyClosedTabs: false,
init: function ss_init() { init: function ss_init() {
// Get file references // Get file references
this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
@ -79,6 +82,8 @@ SessionStore.prototype = {
observerService.addObserver(this, "browser:purge-session-history", true); observerService.addObserver(this, "browser:purge-session-history", true);
observerService.addObserver(this, "Session:Restore", true); observerService.addObserver(this, "Session:Restore", true);
observerService.addObserver(this, "application-background", true); observerService.addObserver(this, "application-background", true);
observerService.addObserver(this, "ClosedTabs:StartNotifications", true);
observerService.addObserver(this, "ClosedTabs:StopNotifications", true);
break; break;
case "final-ui-startup": case "final-ui-startup":
observerService.removeObserver(this, "final-ui-startup"); observerService.removeObserver(this, "final-ui-startup");
@ -157,6 +162,13 @@ SessionStore.prototype = {
// pending save state to ensure that this data does not get lost. // pending save state to ensure that this data does not get lost.
this.flushPendingState(); this.flushPendingState();
break; break;
case "ClosedTabs:StartNotifications":
this._notifyClosedTabs = true;
this._sendClosedTabsToJava(Services.wm.getMostRecentWindow("navigator:browser"));
break;
case "ClosedTabs:StopNotifications":
this._notifyClosedTabs = false;
break;
} }
}, },
@ -291,6 +303,10 @@ SessionStore.prototype = {
this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo); this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo);
this._lastClosedTabIndex = aTabIndex; this._lastClosedTabIndex = aTabIndex;
if (this._notifyClosedTabs) {
this._sendClosedTabsToJava(aWindow);
}
} }
}, },
@ -864,6 +880,10 @@ SessionStore.prototype = {
// Put back the extra data // Put back the extra data
tab.browser.__SS_extdata = closedTab.extData; tab.browser.__SS_extdata = closedTab.extData;
if (this._notifyClosedTabs) {
this._sendClosedTabsToJava(aWindow);
}
return tab.browser; return tab.browser;
}, },
@ -885,6 +905,30 @@ SessionStore.prototype = {
if (aIndex == 0) { if (aIndex == 0) {
this._lastClosedTabIndex = -1; this._lastClosedTabIndex = -1;
} }
if (this._notifyClosedTabs) {
this._sendClosedTabsToJava(aWindow);
}
},
_sendClosedTabsToJava: function ss_sendClosedTabsToJava(aWindow) {
if (!aWindow.__SSID)
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
let closedTabs = this._windows[aWindow.__SSID].closedTabs;
let tabs = closedTabs.map(function (tab) {
// Get the url and title for the last entry in the session history.
let lastEntry = tab.entries[tab.entries.length - 1];
return {
url: lastEntry.url,
title: lastEntry.title || ""
};
});
sendMessageToJava({
type: "ClosedTabs:Data",
tabs: tabs
});
}, },
getTabValue: function ss_getTabValue(aTab, aKey) { getTabValue: function ss_getTabValue(aTab, aKey) {

Просмотреть файл

@ -65,12 +65,16 @@ let Accounts = Object.freeze({
* Fire-and-forget: open the Firefox accounts activity, which * Fire-and-forget: open the Firefox accounts activity, which
* will be the Getting Started screen if FxA isn't yet set up. * will be the Getting Started screen if FxA isn't yet set up.
* *
* Optional extras are passed, as a JSON string, to the Firefox
* Account Getting Started activity in the extras bundle of the
* activity launch intent, under the key "extras".
*
* There is no return value from this method. * There is no return value from this method.
*/ */
launchSetup: function () { launchSetup: function (extras) {
sendMessageToJava({ sendMessageToJava({
type: "Accounts:Create", type: "Accounts:Create",
extras: extras,
}); });
}, },
}); });

Просмотреть файл

@ -117,6 +117,9 @@
contentDescription text, and it should not be translated. --> contentDescription text, and it should not be translated. -->
<string name="fxaccount_empty_contentDescription"></string> <string name="fxaccount_empty_contentDescription"></string>
<string name="fxaccount_custom_server_account_title">&fxaccount_custom_server_account_title;</string>
<string name="fxaccount_custom_server_sync_title">&fxaccount_custom_server_sync_title;</string>
<string name="fxaccount_email_hint">&fxaccount_email_hint;</string> <string name="fxaccount_email_hint">&fxaccount_email_hint;</string>
<string name="fxaccount_password_hint">&fxaccount_password_hint;</string> <string name="fxaccount_password_hint">&fxaccount_password_hint;</string>
<string name="fxaccount_password_hide">&fxaccount_password_hide;</string> <string name="fxaccount_password_hide">&fxaccount_password_hide;</string>
@ -169,7 +172,9 @@
<string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string> <string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string>
<string name="fxaccount_status_header">&fxaccount_status_header2;</string> <string name="fxaccount_status_header">&fxaccount_status_header2;</string>
<string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string> <string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string>
<string name="fxaccount_status_auth_server">&fxaccount_status_auth_server;</string>
<string name="fxaccount_status_device_name">&fxaccount_status_device_name;</string> <string name="fxaccount_status_device_name">&fxaccount_status_device_name;</string>
<string name="fxaccount_status_sync_server">&fxaccount_status_sync_server;</string>
<string name="fxaccount_status_sync">&fxaccount_status_sync;</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_sync_enabled">&fxaccount_status_sync_enabled;</string>
<string name="fxaccount_status_needs_verification">&fxaccount_status_needs_verification2;</string> <string name="fxaccount_status_needs_verification">&fxaccount_status_needs_verification2;</string>

Просмотреть файл

@ -47,6 +47,7 @@ BACKGROUND_TESTS_JAVA_FILES := \
src/helpers/DBProviderTestCase.java \ src/helpers/DBProviderTestCase.java \
src/helpers/FakeProfileTestCase.java \ src/helpers/FakeProfileTestCase.java \
src/nativecode/test/TestNativeCrypto.java \ src/nativecode/test/TestNativeCrypto.java \
src/sync/AndroidSyncTestCaseWithAccounts.java \
src/sync/helpers/BookmarkHelpers.java \ src/sync/helpers/BookmarkHelpers.java \
src/sync/helpers/DefaultBeginDelegate.java \ src/sync/helpers/DefaultBeginDelegate.java \
src/sync/helpers/DefaultCleanDelegate.java \ src/sync/helpers/DefaultCleanDelegate.java \

Просмотреть файл

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:orientation="vertical" > android:orientation="vertical" >
<TextView <TextView
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/app_name" /> android:text="@string/app_name" />

Просмотреть файл

@ -3,30 +3,42 @@
package org.mozilla.gecko.background.fxa.authenticator; package org.mozilla.gecko.background.fxa.authenticator;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; import org.mozilla.gecko.background.sync.AndroidSyncTestCaseWithAccounts;
import org.mozilla.gecko.background.sync.TestSyncAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AccountPickler; import org.mozilla.gecko.fxa.authenticator.AccountPickler;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Separated; import org.mozilla.gecko.fxa.login.Separated;
import org.mozilla.gecko.fxa.login.State; import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.Utils;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.test.InstrumentationTestCase;
import android.test.RenamingDelegatingContext; import android.test.RenamingDelegatingContext;
public class TestAccountPickler extends AndroidSyncTestCase { public class TestAccountPickler extends AndroidSyncTestCaseWithAccounts {
private static final String TEST_TOKEN_SERVER_URI = "tokenServerURI";
private static final String TEST_AUTH_SERVER_URI = "serverURI";
private static final String TEST_PROFILE = "profile";
private final static String FILENAME_PREFIX = "TestAccountPickler-"; private final static String FILENAME_PREFIX = "TestAccountPickler-";
private final static String PICKLE_FILENAME = "pickle"; private final static String PICKLE_FILENAME = "pickle";
private final static String TEST_ACCOUNTTYPE = FxAccountConstants.ACCOUNT_TYPE;
// Test account names must start with TEST_USERNAME in order to be recognized
// as test accounts and deleted in tearDown.
public static final String TEST_USERNAME = "testFirefoxAccount@mozilla.com";
public Account account; public Account account;
public RenamingDelegatingContext context; public RenamingDelegatingContext context;
public AccountManager accountManager;
public TestAccountPickler() {
super(TEST_ACCOUNTTYPE, TEST_USERNAME);
}
@Override @Override
public void setUp() { public void setUp() {
super.setUp();
this.account = null; this.account = null;
// Randomize the filename prefix in case we don't clean up correctly. // Randomize the filename prefix in case we don't clean up correctly.
this.context = new RenamingDelegatingContext(getApplicationContext(), FILENAME_PREFIX + this.context = new RenamingDelegatingContext(getApplicationContext(), FILENAME_PREFIX +
@ -36,54 +48,72 @@ public class TestAccountPickler extends AndroidSyncTestCase {
@Override @Override
public void tearDown() { public void tearDown() {
if (this.account != null) { super.tearDown();
deleteAccount(this, this.accountManager, this.account);
this.account = null;
}
this.context.deleteFile(PICKLE_FILENAME); this.context.deleteFile(PICKLE_FILENAME);
} }
public static void deleteAccount(final InstrumentationTestCase test, public AndroidFxAccount addTestAccount() throws Exception {
final AccountManager accountManager, final Account account) { final State state = new Separated(TEST_USERNAME, "uid", false); // State choice is arbitrary.
TestSyncAccounts.deleteAccount(test, accountManager, account); final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context, TEST_USERNAME,
} TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, state);
private boolean accountsExist() {
// Note that we don't use FirefoxAccounts.firefoxAccountsExist because it unpickles.
return AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE).length > 0;
}
public AndroidFxAccount addDummyAccount() throws Exception {
final String email = "iu@fakedomain.io";
final State state = new Separated(email, "uid", false); // State choice is arbitrary.
final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context, email,
"profile", "serverURI", "tokenServerURI", state);
assertNotNull(account); assertNotNull(account);
assertTrue(accountsExist()); // Sanity check. assertNotNull(account.getProfile());
assertTrue(testAccountsExist()); // Sanity check.
this.account = account.getAndroidAccount(); // To remove in tearDown() if we throw. this.account = account.getAndroidAccount(); // To remove in tearDown() if we throw.
return account; return account;
} }
public void testPickle() throws Exception {
final AndroidFxAccount account = addTestAccount();
// Sync is enabled by default so we do a more thorough test by disabling it.
account.disableSyncing();
final long now = System.currentTimeMillis();
final ExtendedJSONObject o = AccountPickler.toJSON(account, now);
assertEquals(2, o.getLong(AccountPickler.KEY_PICKLE_VERSION).longValue());
assertTrue(o.getLong(AccountPickler.KEY_PICKLE_TIMESTAMP).longValue() < System.currentTimeMillis());
assertEquals(AndroidFxAccount.CURRENT_ACCOUNT_VERSION, o.getIntegerSafely(AccountPickler.KEY_ACCOUNT_VERSION).intValue());
assertEquals(FxAccountConstants.ACCOUNT_TYPE, o.getString(AccountPickler.KEY_ACCOUNT_TYPE));
assertEquals(TEST_USERNAME, o.getString(AccountPickler.KEY_EMAIL));
assertEquals(TEST_PROFILE, o.getString(AccountPickler.KEY_PROFILE));
assertEquals(TEST_AUTH_SERVER_URI, o.getString(AccountPickler.KEY_IDP_SERVER_URI));
assertEquals(TEST_TOKEN_SERVER_URI, o.getString(AccountPickler.KEY_TOKEN_SERVER_URI));
assertFalse(o.getBoolean(AccountPickler.KEY_IS_SYNCING_ENABLED));
assertNotNull(o.get(AccountPickler.KEY_BUNDLE));
}
public void testPickleAndUnpickle() throws Exception { public void testPickleAndUnpickle() throws Exception {
final AndroidFxAccount inputAccount = addDummyAccount(); final AndroidFxAccount inputAccount = addTestAccount();
// Sync is enabled by default so we do a more thorough test by disabling it. // Sync is enabled by default so we do a more thorough test by disabling it.
inputAccount.disableSyncing(); inputAccount.disableSyncing();
AccountPickler.pickle(inputAccount, PICKLE_FILENAME); AccountPickler.pickle(inputAccount, PICKLE_FILENAME);
final ExtendedJSONObject inputJSON = AccountPickler.toJSON(inputAccount, 0);
final State inputState = inputAccount.getState();
assertNotNull(inputJSON);
assertNotNull(inputState);
// unpickle adds an account to the AccountManager so delete it first. // unpickle adds an account to the AccountManager so delete it first.
deleteAccount(this, this.accountManager, inputAccount.getAndroidAccount()); deleteTestAccounts();
assertFalse(accountsExist()); assertFalse(testAccountsExist());
final AndroidFxAccount unpickledAccount = final AndroidFxAccount unpickledAccount = AccountPickler.unpickle(context, PICKLE_FILENAME);
AccountPickler.unpickle(context, PICKLE_FILENAME);
assertNotNull(unpickledAccount); assertNotNull(unpickledAccount);
this.account = unpickledAccount.getAndroidAccount(); // To remove in tearDown(). final ExtendedJSONObject unpickledJSON = AccountPickler.toJSON(unpickledAccount, 0);
assertAccountsEquals(inputAccount, unpickledAccount); final State unpickledState = unpickledAccount.getState();
assertNotNull(unpickledJSON);
assertNotNull(unpickledState);
assertEquals(inputJSON, unpickledJSON);
assertStateEquals(inputState, unpickledState);
} }
public void testDeletePickle() throws Exception { public void testDeletePickle() throws Exception {
final AndroidFxAccount account = addDummyAccount(); final AndroidFxAccount account = addTestAccount();
AccountPickler.pickle(account, PICKLE_FILENAME); AccountPickler.pickle(account, PICKLE_FILENAME);
final String s = Utils.readFile(context, PICKLE_FILENAME); final String s = Utils.readFile(context, PICKLE_FILENAME);
@ -91,23 +121,7 @@ public class TestAccountPickler extends AndroidSyncTestCase {
assertTrue(s.length() > 0); assertTrue(s.length() > 0);
AccountPickler.deletePickle(context, PICKLE_FILENAME); AccountPickler.deletePickle(context, PICKLE_FILENAME);
org.mozilla.gecko.background.sync.TestAccountPickler.assertFileNotPresent( assertFileNotPresent(context, PICKLE_FILENAME);
context, PICKLE_FILENAME);
}
private void assertAccountsEquals(final AndroidFxAccount expected,
final AndroidFxAccount actual) throws Exception {
// TODO: Write and use AndroidFxAccount.equals
// TODO: protected.
//assertEquals(expected.getAccountVersion(), actual.getAccountVersion());
assertEquals(expected.getProfile(), actual.getProfile());
assertEquals(expected.getAccountServerURI(), actual.getAccountServerURI());
assertEquals(expected.getAudience(), actual.getAudience());
assertEquals(expected.getTokenServerURI(), actual.getTokenServerURI());
assertEquals(expected.getSyncPrefsPath(), actual.getSyncPrefsPath());
assertEquals(expected.isSyncing(), actual.isSyncing());
assertEquals(expected.getEmail(), actual.getEmail());
assertStateEquals(expected.getState(), actual.getState());
} }
private void assertStateEquals(final State expected, final State actual) throws Exception { private void assertStateEquals(final State expected, final State actual) throws Exception {

Просмотреть файл

@ -10,7 +10,6 @@ import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.test.mock.MockContext;
public class TestHealthReportPruneService public class TestHealthReportPruneService
extends BackgroundServiceTestCase<TestHealthReportPruneService.MockHealthReportPruneService> { extends BackgroundServiceTestCase<TestHealthReportPruneService.MockHealthReportPruneService> {
@ -37,7 +36,10 @@ public class TestHealthReportPruneService
@Override @Override
public PrunePolicy getPrunePolicy(final String profilePath) { public PrunePolicy getPrunePolicy(final String profilePath) {
final PrunePolicyStorage storage = new PrunePolicyDatabaseStorage(new MockContext(), profilePath); // We don't actually need any storage, so just make it null. Actually
// creating storage requires a valid context; here, we only have a
// MockContext.
final PrunePolicyStorage storage = null;
prunePolicy = new MockPrunePolicy(storage, getSharedPreferences()); prunePolicy = new MockPrunePolicy(storage, getSharedPreferences());
return prunePolicy; return prunePolicy;
} }

Просмотреть файл

@ -21,6 +21,12 @@ public class TestHealthReportUploadService
GlobalConstants.SHARED_PREFERENCES_MODE); GlobalConstants.SHARED_PREFERENCES_MODE);
} }
@Override
public boolean backgroundDataIsEnabled() {
// When testing, we always want to say we can upload.
return true;
}
@Override @Override
public void onHandleIntent(Intent intent) { public void onHandleIntent(Intent intent) {
super.onHandleIntent(intent); super.onHandleIntent(intent);

Просмотреть файл

@ -23,7 +23,7 @@ public class AndroidSyncTestCase extends ActivityInstrumentationTestCase2<Activi
} }
public Context getApplicationContext() { public Context getApplicationContext() {
return this.getInstrumentation().getTargetContext().getApplicationContext(); return this.getInstrumentation().getTargetContext();
} }
public static void performWait(Runnable runnable) { public static void performWait(Runnable runnable) {

Просмотреть файл

@ -0,0 +1,83 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
public class AndroidSyncTestCaseWithAccounts extends AndroidSyncTestCase {
public final String testAccountType;
public final String testAccountPrefix;
protected Context context;
protected AccountManager accountManager;
protected int numAccounts;
public AndroidSyncTestCaseWithAccounts(String accountType, String accountPrefix) {
super();
this.testAccountType = accountType;
this.testAccountPrefix = accountPrefix;
}
@Override
public void setUp() {
context = getApplicationContext();
accountManager = AccountManager.get(context);
deleteTestAccounts(); // Always start with no test accounts.
numAccounts = accountManager.getAccountsByType(testAccountType).length;
}
public List<Account> getTestAccounts() {
final List<Account> testAccounts = new ArrayList<Account>();
final Account[] accounts = accountManager.getAccountsByType(testAccountType);
for (Account account : accounts) {
if (account.name.startsWith(testAccountPrefix)) {
testAccounts.add(account);
}
}
return testAccounts;
}
public void deleteTestAccounts() {
for (Account account : getTestAccounts()) {
TestSyncAccounts.deleteAccount(this, accountManager, account);
}
}
public boolean testAccountsExist() {
// Note that we don't use FirefoxAccounts.firefoxAccountsExist because it unpickles.
return !getTestAccounts().isEmpty();
}
@Override
public void tearDown() {
deleteTestAccounts();
assertEquals(numAccounts, accountManager.getAccountsByType(testAccountType).length);
}
public static void assertFileNotPresent(final Context context, final String filename) throws Exception {
// Verify file is not present.
FileInputStream fis = null;
try {
fis = context.openFileInput(filename);
fail("Should get FileNotFoundException.");
} catch (FileNotFoundException e) {
// Do nothing; file should not exist.
} finally {
if (fis != null) {
fis.close();
}
}
}
}

Просмотреть файл

@ -3,15 +3,10 @@
package org.mozilla.gecko.background.sync; package org.mozilla.gecko.background.sync;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.background.common.GlobalConstants; import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.sync.SyncConfiguration;
@ -28,7 +23,7 @@ import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
public class TestAccountPickler extends AndroidSyncTestCase { public class TestAccountPickler extends AndroidSyncTestCaseWithAccounts {
public static final String TEST_FILENAME = "test.json"; public static final String TEST_FILENAME = "test.json";
public static final String TEST_ACCOUNTTYPE = SyncConstants.ACCOUNTTYPE_SYNC; public static final String TEST_ACCOUNTTYPE = SyncConstants.ACCOUNTTYPE_SYNC;
@ -49,56 +44,17 @@ public class TestAccountPickler extends AndroidSyncTestCase {
public static final long TEST_VERSION = SyncConfiguration.CURRENT_PREFS_VERSION; public static final long TEST_VERSION = SyncConfiguration.CURRENT_PREFS_VERSION;
protected SyncAccountParameters params; protected SyncAccountParameters params;
protected Context context;
protected AccountManager accountManager;
protected int numAccounts;
public TestAccountPickler() {
super(TEST_ACCOUNTTYPE, TEST_USERNAME);
}
@Override
public void setUp() { public void setUp() {
context = getApplicationContext(); super.setUp();
accountManager = AccountManager.get(context);
params = new SyncAccountParameters(context, accountManager, params = new SyncAccountParameters(context, accountManager,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVER_URL, TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVER_URL,
TEST_CLUSTER_URL, TEST_CLIENT_NAME, TEST_CLIENT_GUID); TEST_CLUSTER_URL, TEST_CLIENT_NAME, TEST_CLIENT_GUID);
numAccounts = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
}
public static List<Account> getTestAccounts(final AccountManager accountManager) {
final List<Account> testAccounts = new ArrayList<Account>();
final Account[] accounts = accountManager.getAccountsByType(TEST_ACCOUNTTYPE);
for (Account account : accounts) {
if (account.name.startsWith(TEST_USERNAME)) {
testAccounts.add(account);
}
}
return testAccounts;
}
public void deleteTestAccounts() {
for (Account account : getTestAccounts(accountManager)) {
TestSyncAccounts.deleteAccount(this, accountManager, account);
}
}
public void tearDown() {
deleteTestAccounts();
assertEquals(numAccounts, accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length);
}
public static void assertFileNotPresent(final Context context, final String filename) throws Exception {
// Verify file is not present.
FileInputStream fis = null;
try {
fis = context.openFileInput(TEST_FILENAME);
fail("Should get FileNotFoundException.");
} catch (FileNotFoundException e) {
// Do nothing; file should not exist.
} finally {
if (fis != null) {
fis.close();
}
}
} }
public void testPersist() throws Exception { public void testPersist() throws Exception {
@ -137,7 +93,7 @@ public class TestAccountPickler extends AndroidSyncTestCase {
public Account deleteAccountsAndUnpickle(final Context context, final AccountManager accountManager, final String filename) { public Account deleteAccountsAndUnpickle(final Context context, final AccountManager accountManager, final String filename) {
deleteTestAccounts(); deleteTestAccounts();
assertEquals(0, getTestAccounts(accountManager).size()); assertEquals(0, getTestAccounts().size());
return AccountPickler.unpickle(context, filename); return AccountPickler.unpickle(context, filename);
} }
@ -150,7 +106,7 @@ public class TestAccountPickler extends AndroidSyncTestCase {
assertNotNull(account); assertNotNull(account);
try { try {
assertEquals(1, getTestAccounts(accountManager).size()); assertEquals(1, getTestAccounts().size());
assertTrue(ContentResolver.getSyncAutomatically(account, BrowserContract.AUTHORITY)); assertTrue(ContentResolver.getSyncAutomatically(account, BrowserContract.AUTHORITY));
assertEquals(account.name, TEST_USERNAME); assertEquals(account.name, TEST_USERNAME);
@ -186,7 +142,7 @@ public class TestAccountPickler extends AndroidSyncTestCase {
assertNotNull(account); assertNotNull(account);
try { try {
assertEquals(1, getTestAccounts(accountManager).size()); assertEquals(1, getTestAccounts().size());
assertFalse(ContentResolver.getSyncAutomatically(account, BrowserContract.AUTHORITY)); assertFalse(ContentResolver.getSyncAutomatically(account, BrowserContract.AUTHORITY));
} finally { } finally {
TestSyncAccounts.deleteAccount(this, accountManager, account); TestSyncAccounts.deleteAccount(this, accountManager, account);
@ -241,7 +197,7 @@ public class TestAccountPickler extends AndroidSyncTestCase {
// nothing was unpickled by verifying that the username has not changed. // nothing was unpickled by verifying that the username has not changed.
assertTrue(SyncAccounts.syncAccountsExist(context)); assertTrue(SyncAccounts.syncAccountsExist(context));
for (Account a : getTestAccounts(accountManager)) { for (Account a : getTestAccounts()) {
assertEquals(TEST_USERNAME, a.name); assertEquals(TEST_USERNAME, a.name);
} }
} }

Просмотреть файл

@ -44,7 +44,8 @@ public class TestSyncConfiguration extends AndroidSyncTestCase {
config.persistToPrefs(); config.persistToPrefs();
assertFalse(prefs.contains(SyncConfiguration.PREF_DECLINED_ENGINE_NAMES)); assertFalse(prefs.contains(SyncConfiguration.PREF_DECLINED_ENGINE_NAMES));
config = newSyncConfiguration(); config = newSyncConfiguration();
assertNull(config.declinedEngineNames); assertNotNull(config.declinedEngineNames);
assertTrue(config.declinedEngineNames.isEmpty());
} }
public void testEnabledEngineNames() { public void testEnabledEngineNames() {

Просмотреть файл

@ -13,6 +13,7 @@ import types
from collections import namedtuple from collections import namedtuple
import mozwebidlcodegen import mozwebidlcodegen
from reftest import ReftestManifest
import mozbuild.makeutil as mozmakeutil import mozbuild.makeutil as mozmakeutil
from mozpack.copier import FilePurger from mozpack.copier import FilePurger
@ -1075,6 +1076,11 @@ class RecursiveMakeBackend(CommonBackend):
(obj.install_prefix, set())) (obj.install_prefix, set()))
m[1].add(obj.manifest_obj_relpath) m[1].add(obj.manifest_obj_relpath)
if isinstance(obj.manifest, ReftestManifest):
# Mark included files as part of the build backend so changes
# result in re-config.
self.backend_input_files |= obj.manifest.manifests
def _process_local_include(self, local_include, backend_file): def _process_local_include(self, local_include, backend_file):
if local_include.startswith('/'): if local_include.startswith('/'):
path = '$(topsrcdir)' path = '$(topsrcdir)'

Просмотреть файл

@ -15,7 +15,7 @@ from mach.mixin.logging import LoggingMixin
import mozpack.path as mozpath import mozpack.path as mozpath
import manifestparser import manifestparser
import reftest
import mozinfo import mozinfo
from .data import ( from .data import (
@ -419,6 +419,11 @@ class TreeMetadataEmitter(LoggingMixin):
for obj in self._process_test_manifest(sandbox, info, path): for obj in self._process_test_manifest(sandbox, info, path):
yield obj yield obj
for flavor in ('crashtest', 'reftest'):
for path in sandbox.get('%s_MANIFESTS' % flavor.upper(), []):
for obj in self._process_reftest_manifest(sandbox, flavor, path):
yield obj
jar_manifests = sandbox.get('JAR_MANIFESTS', []) jar_manifests = sandbox.get('JAR_MANIFESTS', [])
if len(jar_manifests) > 1: if len(jar_manifests) > 1:
raise SandboxValidationError('While JAR_MANIFESTS is a list, ' raise SandboxValidationError('While JAR_MANIFESTS is a list, '
@ -601,6 +606,38 @@ class TreeMetadataEmitter(LoggingMixin):
'manifest file %s: %s' % (path, 'manifest file %s: %s' % (path,
'\n'.join(traceback.format_exception(*sys.exc_info())))) '\n'.join(traceback.format_exception(*sys.exc_info()))))
def _process_reftest_manifest(self, sandbox, flavor, manifest_path):
manifest_path = mozpath.normpath(manifest_path)
manifest_full_path = mozpath.normpath(mozpath.join(
sandbox['SRCDIR'], manifest_path))
manifest_reldir = mozpath.dirname(mozpath.relpath(manifest_full_path,
sandbox['TOPSRCDIR']))
manifest = reftest.ReftestManifest()
manifest.load(manifest_full_path)
# reftest manifests don't come from manifest parser. But they are
# similar enough that we can use the same emitted objects. Note
# that we don't perform any installs for reftests.
obj = TestManifest(sandbox, manifest_full_path, manifest,
flavor=flavor, install_prefix='%s/' % flavor,
relpath=mozpath.join(manifest_reldir,
mozpath.basename(manifest_path)))
for test in sorted(manifest.files):
obj.tests.append({
'path': test,
'here': mozpath.dirname(test),
'manifest': manifest_full_path,
'name': mozpath.basename(test),
'head': '',
'tail': '',
'support-files': '',
'subsuite': '',
})
yield obj
def _emit_directory_traversal_from_sandbox(self, sandbox): def _emit_directory_traversal_from_sandbox(self, sandbox):
o = DirectoryTraversal(sandbox) o = DirectoryTraversal(sandbox)
o.dirs = sandbox.get('DIRS', []) o.dirs = sandbox.get('DIRS', [])

Просмотреть файл

@ -599,6 +599,12 @@ VARIABLES = {
"""List of manifest files defining browser chrome tests. """List of manifest files defining browser chrome tests.
""", None), """, None),
'CRASHTEST_MANIFESTS': (StrictOrderingOnAppendList, list,
"""List of manifest files defining crashtests.
These are commonly named crashtests.list.
""", None),
'METRO_CHROME_MANIFESTS': (StrictOrderingOnAppendList, list, 'METRO_CHROME_MANIFESTS': (StrictOrderingOnAppendList, list,
"""List of manifest files defining metro browser chrome tests. """List of manifest files defining metro browser chrome tests.
""", None), """, None),
@ -615,6 +621,12 @@ VARIABLES = {
"""List of manifest files defining webapprt mochitest chrome tests. """List of manifest files defining webapprt mochitest chrome tests.
""", None), """, None),
'REFTEST_MANIFESTS': (StrictOrderingOnAppendList, list,
"""List of manifest files defining reftests.
These are commonly named reftest.list.
""", None),
'WEBRTC_SIGNALLING_TEST_MANIFESTS': (StrictOrderingOnAppendList, list, 'WEBRTC_SIGNALLING_TEST_MANIFESTS': (StrictOrderingOnAppendList, list,
"""List of manifest files defining WebRTC signalling tests. """List of manifest files defining WebRTC signalling tests.
""", None), """, None),

Просмотреть файл

@ -0,0 +1 @@
== crashtest1.html crashtest1-ref.html

Просмотреть файл

@ -7,3 +7,5 @@ METRO_CHROME_MANIFESTS += ['metro.ini']
MOCHITEST_MANIFESTS += ['mochitest.ini'] MOCHITEST_MANIFESTS += ['mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['chrome.ini'] MOCHITEST_CHROME_MANIFESTS += ['chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['xpcshell.ini'] XPCSHELL_TESTS_MANIFESTS += ['xpcshell.ini']
REFTEST_MANIFESTS += ['reftest.list']
CRASHTEST_MANIFESTS += ['crashtest.list']

Просмотреть файл

@ -0,0 +1 @@
== reftest1.html reftest1-ref.html

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше