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

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

View file

@ -0,0 +1,596 @@
/* -*- 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;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.util.Locale;
import java.util.UUID;
import java.util.regex.Pattern;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
public final class ANRReporter extends BroadcastReceiver
{
private static final boolean DEBUG = false;
private static final String LOGTAG = "GeckoANRReporter";
private static final String ANR_ACTION = "android.intent.action.ANR";
// Number of lines to search traces.txt to decide whether it's a Gecko ANR
private static final int LINES_TO_IDENTIFY_TRACES = 10;
// ANRs may happen because of memory pressure,
// so don't use up too much memory here
// Size of buffer to hold one line of text
private static final int TRACES_LINE_SIZE = 100;
// Size of block to use when processing traces.txt
private static final int TRACES_BLOCK_SIZE = 2000;
private static final String TRACES_CHARSET = "utf-8";
private static final String PING_CHARSET = "utf-8";
private static final ANRReporter sInstance = new ANRReporter();
private static int sRegisteredCount;
private Handler mHandler;
private volatile boolean mPendingANR;
@WrapForJNI
private static native boolean requestNativeStack(boolean unwind);
@WrapForJNI
private static native String getNativeStack();
@WrapForJNI
private static native void releaseNativeStack();
public static void register(Context context) {
if (sRegisteredCount++ != 0) {
// Already registered
return;
}
sInstance.start(context);
}
public static void unregister() {
if (sRegisteredCount == 0) {
Log.w(LOGTAG, "register/unregister mismatch");
return;
}
if (--sRegisteredCount != 0) {
// Should still be registered
return;
}
sInstance.stop();
}
private void start(final Context context) {
Thread receiverThread = new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
synchronized (ANRReporter.this) {
mHandler = new Handler();
ANRReporter.this.notify();
}
if (DEBUG) {
Log.d(LOGTAG, "registering receiver");
}
context.registerReceiver(ANRReporter.this,
new IntentFilter(ANR_ACTION),
null,
mHandler);
Looper.loop();
if (DEBUG) {
Log.d(LOGTAG, "unregistering receiver");
}
context.unregisterReceiver(ANRReporter.this);
mHandler = null;
}
}, LOGTAG);
receiverThread.setDaemon(true);
receiverThread.start();
}
private void stop() {
synchronized (this) {
while (mHandler == null) {
try {
wait(1000);
if (mHandler == null) {
// We timed out; just give up. The process is probably
// quitting anyways, so we let the OS do the clean up
Log.w(LOGTAG, "timed out waiting for handler");
return;
}
} catch (InterruptedException e) {
}
}
}
Looper looper = mHandler.getLooper();
looper.quit();
try {
looper.getThread().join();
} catch (InterruptedException e) {
}
}
private ANRReporter() {
}
// Return the "traces.txt" file, or null if there is no such file
private static File getTracesFile() {
// Check most common location first.
File tracesFile = new File("/data/anr/traces.txt");
if (tracesFile.isFile() && tracesFile.canRead()) {
return tracesFile;
}
// Find the traces file name if we can.
try {
// getprop [prop-name [default-value]]
Process propProc = (new ProcessBuilder())
.command("/system/bin/getprop", "dalvik.vm.stack-trace-file")
.redirectErrorStream(true)
.start();
try {
BufferedReader buf = new BufferedReader(
new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE);
String propVal = buf.readLine();
if (DEBUG) {
Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal));
}
// getprop can return empty string when the prop value is empty
// or prop is undefined, treat both cases the same way
if (propVal != null && propVal.length() != 0) {
tracesFile = new File(propVal);
if (tracesFile.isFile() && tracesFile.canRead()) {
return tracesFile;
} else if (DEBUG) {
Log.d(LOGTAG, "cannot access traces file");
}
} else if (DEBUG) {
Log.d(LOGTAG, "empty getprop result");
}
} finally {
propProc.destroy();
}
} catch (IOException e) {
Log.w(LOGTAG, e);
} catch (ClassCastException e) {
Log.w(LOGTAG, e); // Bug 975436
}
return null;
}
private static File getPingFile() {
if (GeckoAppShell.getContext() == null) {
return null;
}
GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
if (profile == null) {
return null;
}
File profDir = profile.getDir();
if (profDir == null) {
return null;
}
File pingDir = new File(profDir, "saved-telemetry-pings");
pingDir.mkdirs();
if (!(pingDir.exists() && pingDir.isDirectory())) {
return null;
}
return new File(pingDir, UUID.randomUUID().toString());
}
// Return true if the traces file corresponds to a Gecko ANR
private static boolean isGeckoTraces(String pkgName, File tracesFile) {
try {
final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)";
// Regex for finding our package name in the traces file
Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME);
Pattern mangledPattern = null;
if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) {
mangledPattern = Pattern.compile(Pattern.quote(
AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME);
}
if (DEBUG) {
Log.d(LOGTAG, "trying to match package: " + pkgName);
}
BufferedReader traces = new BufferedReader(
new FileReader(tracesFile), TRACES_BLOCK_SIZE);
try {
for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) {
String line = traces.readLine();
if (DEBUG) {
Log.d(LOGTAG, "identifying line: " + String.valueOf(line));
}
if (line == null) {
if (DEBUG) {
Log.d(LOGTAG, "reached end of traces file");
}
return false;
}
if (pkgPattern.matcher(line).find()) {
// traces.txt file contains our package
return true;
}
if (mangledPattern != null && mangledPattern.matcher(line).find()) {
// traces.txt file contains our alternate package
return true;
}
}
} finally {
traces.close();
}
} catch (IOException e) {
// meh, can't even read from it right. just return false
}
return false;
}
private static long getUptimeMins() {
long uptimeMins = (new File("/proc/self/stat")).lastModified();
if (uptimeMins != 0L) {
uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L;
if (DEBUG) {
Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins));
}
return uptimeMins;
}
if (DEBUG) {
Log.d(LOGTAG, "could not get uptime");
}
return 0L;
}
/*
a saved telemetry ping file consists of JSON in the following format,
{
"reason": "android-anr-report",
"slug": "<uuid-string>",
"payload": <json-object>
}
for Android ANR, our JSON payload should look like,
{
"ver": 1,
"simpleMeasurements": {
"uptime": <uptime>
},
"info": {
"reason": "android-anr-report",
"OS": "Android",
...
},
"androidANR": "...",
"androidLogcat": "..."
}
*/
private static int writePingPayload(OutputStream ping,
String payload) throws IOException {
byte [] data = payload.getBytes(PING_CHARSET);
ping.write(data);
return data.length;
}
private static void fillPingHeader(OutputStream ping, String slug)
throws IOException {
// ping file header
byte [] data = ("{" +
"\"reason\":\"android-anr-report\"," +
"\"slug\":" + JSONObject.quote(slug) + "," +
"\"payload\":").getBytes(PING_CHARSET);
ping.write(data);
if (DEBUG) {
Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length));
}
// payload start
int size = writePingPayload(ping, ("{" +
"\"ver\":1," +
"\"simpleMeasurements\":{" +
"\"uptime\":" + String.valueOf(getUptimeMins()) +
"}," +
"\"info\":{" +
"\"reason\":\"android-anr-report\"," +
"\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," +
"\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," +
"\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," +
"\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION) + "," +
"\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," +
"\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
"\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," +
// Technically the platform build ID may be different, but we'll never know
"\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
"\"locale\":" + JSONObject.quote(Locales.getLanguageTag(Locale.getDefault())) + "," +
"\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," +
"\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," +
"\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," +
"\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," +
"\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," +
"\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," +
"\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) +
"}," +
"\"androidANR\":\""));
if (DEBUG) {
Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size));
}
// We are at the start of ANR data
}
// Block is a section of the larger input stream, and we want to find pattern within
// the stream. This is straightforward if the entire pattern is within one block;
// however, if the pattern spans across two blocks, we have to match both the start of
// the pattern in the first block and the end of the pattern in the second block.
// * If pattern is found in block, this method returns the index at the end of the
// found pattern, which must always be > 0.
// * If pattern is not found, it returns 0.
// * If the start of the pattern matches the end of the block, it returns a number
// < 0, which equals the negated value of how many characters in pattern are already
// matched; when processing the next block, this number is passed in through
// prevIndex, and the rest of the characters in pattern are matched against the
// start of this second block. The method returns value > 0 if the rest of the
// characters match, or 0 if they do not.
private static int getEndPatternIndex(String block, String pattern, int prevIndex) {
if (pattern == null || block.length() < pattern.length()) {
// Nothing to do
return 0;
}
if (prevIndex < 0) {
// Last block ended with a partial start; now match start of block to rest of pattern
if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) {
// Rest of pattern matches; return index at end of pattern
return pattern.length() + prevIndex;
}
// Not a match; continue with normal search
}
// Did not find pattern in last block; see if entire pattern is inside this block
int index = block.indexOf(pattern);
if (index >= 0) {
// Found pattern; return index at end of the pattern
return index + pattern.length();
}
// Block does not contain the entire pattern, but see if the end of the block
// contains the start of pattern. To do that, we see if block ends with the
// first n-1 characters of pattern, the first n-2 characters of pattern, etc.
for (index = block.length() - pattern.length() + 1; index < block.length(); index++) {
// Using index as a start, see if the rest of block contains the start of pattern
if (block.charAt(index) == pattern.charAt(0) &&
block.endsWith(pattern.substring(0, block.length() - index))) {
// Found partial match; return -(number of characters matched),
// i.e. -1 for 1 character matched, -2 for 2 characters matched, etc.
return index - block.length();
}
}
return 0;
}
// Copy the content of reader to ping;
// copying stops when endPattern is found in the input stream
private static int fillPingBlock(OutputStream ping,
Reader reader, String endPattern)
throws IOException {
int total = 0;
int endIndex = 0;
char [] block = new char[TRACES_BLOCK_SIZE];
for (int size = reader.read(block); size >= 0; size = reader.read(block)) {
String stringBlock = new String(block, 0, size);
endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex);
if (endIndex > 0) {
// Found end pattern; clip the string
stringBlock = stringBlock.substring(0, endIndex);
}
String quoted = JSONObject.quote(stringBlock);
total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1));
if (endIndex > 0) {
// End pattern already found; return now
break;
}
}
return total;
}
private static void fillLogcat(final OutputStream ping) {
if (Versions.preJB) {
// Logcat retrieval is not supported on pre-JB devices.
return;
}
try {
// get the last 200 lines of logcat
Process proc = (new ProcessBuilder())
.command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D")
.redirectErrorStream(true)
.start();
try {
Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET);
int size = fillPingBlock(ping, procOut, null);
if (DEBUG) {
Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size));
}
} finally {
proc.destroy();
}
} catch (IOException e) {
// ignore because logcat is not essential
Log.w(LOGTAG, e);
}
}
private static void fillPingFooter(OutputStream ping,
boolean haveNativeStack)
throws IOException {
// We are at the end of ANR data
int total = writePingPayload(ping, ("\"," +
"\"androidLogcat\":\""));
fillLogcat(ping);
if (haveNativeStack) {
total += writePingPayload(ping, ("\"," +
"\"androidNativeStack\":"));
String nativeStack = String.valueOf(getNativeStack());
int size = writePingPayload(ping, nativeStack);
if (DEBUG) {
Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size));
}
total += size + writePingPayload(ping, "}");
} else {
total += writePingPayload(ping, "\"}");
}
byte [] data = (
"}").getBytes(PING_CHARSET);
ping.write(data);
if (DEBUG) {
Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total));
}
}
private static void processTraces(Reader traces, File pingFile) {
// Only get native stack if Gecko is running.
// Also, unwinding is memory intensive, so only unwind if we have enough memory.
final boolean haveNativeStack =
GeckoThread.isRunning() ?
requestNativeStack(/* unwind */ SysInfo.getMemSize() >= 640) : false;
try {
OutputStream ping = new BufferedOutputStream(
new FileOutputStream(pingFile), TRACES_BLOCK_SIZE);
try {
fillPingHeader(ping, pingFile.getName());
// Traces file has the format
// ----- pid xxx at xxx -----
// Cmd line: org.mozilla.xxx
// * stack trace *
// ----- end xxx -----
// ----- pid xxx at xxx -----
// Cmd line: com.android.xxx
// * stack trace *
// ...
// If we end the stack dump at the first end marker,
// only Fennec stacks will be dumped
int size = fillPingBlock(ping, traces, "\n----- end");
if (DEBUG) {
Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size));
}
fillPingFooter(ping, haveNativeStack);
if (DEBUG) {
Log.d(LOGTAG, "finished creating ping file");
}
return;
} finally {
ping.close();
if (haveNativeStack) {
releaseNativeStack();
}
}
} catch (IOException e) {
Log.w(LOGTAG, e);
}
// exception; delete ping file
if (pingFile.exists()) {
pingFile.delete();
}
}
private static void processTraces(File tracesFile, File pingFile) {
try {
Reader traces = new InputStreamReader(
new FileInputStream(tracesFile), TRACES_CHARSET);
try {
processTraces(traces, pingFile);
} finally {
traces.close();
}
} catch (IOException e) {
Log.w(LOGTAG, e);
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (mPendingANR) {
// we already processed an ANR without getting unstuck; skip this one
if (DEBUG) {
Log.d(LOGTAG, "skipping duplicate ANR");
}
return;
}
if (ThreadUtils.getUiHandler() != null) {
mPendingANR = true;
// detect when the main thread gets unstuck
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
// okay to reset mPendingANR on main thread
mPendingANR = false;
if (DEBUG) {
Log.d(LOGTAG, "yay we got unstuck!");
}
}
});
}
if (DEBUG) {
Log.d(LOGTAG, "receiving " + String.valueOf(intent));
}
if (!ANR_ACTION.equals(intent.getAction())) {
return;
}
// make sure we have a good save location first
File pingFile = getPingFile();
if (DEBUG) {
Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile));
}
if (pingFile == null) {
return;
}
File tracesFile = getTracesFile();
if (DEBUG) {
Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile));
}
if (tracesFile == null) {
return;
}
// We get ANR intents from all ANRs in the system, but we only want Gecko ANRs
if (!isGeckoTraces(context.getPackageName(), tracesFile)) {
if (DEBUG) {
Log.d(LOGTAG, "traces is not Gecko ANR");
}
return;
}
Log.i(LOGTAG, "processing Gecko ANR");
processTraces(tracesFile, pingFile);
}
}

View file

@ -0,0 +1,117 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.home.HomeConfig;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.util.StringUtils;
public class AboutPages {
// All of our special pages.
public static final String ACCOUNTS = "about:accounts";
public static final String ADDONS = "about:addons";
public static final String CONFIG = "about:config";
public static final String DOWNLOADS = "about:downloads";
public static final String FIREFOX = "about:firefox";
public static final String HEALTHREPORT = "about:healthreport";
public static final String HOME = "about:home";
public static final String LOGINS = "about:logins";
public static final String PRIVATEBROWSING = "about:privatebrowsing";
public static final String READER = "about:reader";
public static final String UPDATER = "about:";
public static final String URL_FILTER = "about:%";
public static final String PANEL_PARAM = "panel";
public static final boolean isAboutPage(final String url) {
return url != null && url.startsWith("about:");
}
public static final boolean isTitlelessAboutPage(final String url) {
return isAboutHome(url) ||
PRIVATEBROWSING.equals(url);
}
public static final boolean isAboutHome(final String url) {
if (url == null || !url.startsWith(HOME)) {
return false;
}
// We sometimes append a parameter to "about:home" to specify which page to
// show when we open the home pager. Discard this parameter when checking
// whether or not this URL is "about:home".
return HOME.equals(url.split("\\?")[0]);
}
public static final String getPanelIdFromAboutHomeUrl(String aboutHomeUrl) {
return StringUtils.getQueryParameter(aboutHomeUrl, PANEL_PARAM);
}
public static boolean isAboutReader(final String url) {
return isAboutPage(READER, url);
}
public static boolean isAboutConfig(final String url) {
return isAboutPage(CONFIG, url);
}
public static boolean isAboutAddons(final String url) {
return isAboutPage(ADDONS, url);
}
public static boolean isAboutPrivateBrowsing(final String url) {
return isAboutPage(PRIVATEBROWSING, url);
}
public static boolean isAboutPage(String page, String url) {
return url != null && url.toLowerCase().startsWith(page);
}
public static final String[] DEFAULT_ICON_PAGES = new String[] {
HOME,
ACCOUNTS,
ADDONS,
CONFIG,
DOWNLOADS,
FIREFOX,
HEALTHREPORT,
UPDATER
};
public static boolean isBuiltinIconPage(final String url) {
if (url == null ||
!url.startsWith("about:")) {
return false;
}
// about:home uses a separate search built-in icon.
if (isAboutHome(url)) {
return true;
}
// TODO: it'd be quicker to not compare the "about:" part every time.
for (int i = 0; i < DEFAULT_ICON_PAGES.length; ++i) {
if (DEFAULT_ICON_PAGES[i].equals(url)) {
return true;
}
}
return false;
}
/**
* Get a URL that navigates to the specified built-in Home Panel.
*
* @param panelType to navigate to.
* @return URL.
* @throws IllegalArgumentException if the built-in panel type is not a built-in panel.
*/
@RobocopTarget
public static String getURLForBuiltinPanelType(PanelType panelType) throws IllegalArgumentException {
return HOME + "?panel=" + HomeConfig.getIdForBuiltinPanelType(panelType);
}
}

View file

@ -0,0 +1,318 @@
/* -*- 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;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.restrictions.Restrictable;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
/**
* Helper class to manage Android Accounts corresponding to Firefox Accounts.
*/
public class AccountsHelper implements NativeEventListener {
public static final String LOGTAG = "GeckoAccounts";
protected final Context mContext;
protected final GeckoProfile mProfile;
public AccountsHelper(Context context, GeckoProfile profile) {
mContext = context;
mProfile = profile;
EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
if (dispatcher == null) {
Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
return;
}
dispatcher.registerGeckoThreadListener(this,
"Accounts:CreateFirefoxAccountFromJSON",
"Accounts:UpdateFirefoxAccountFromJSON",
"Accounts:Create",
"Accounts:DeleteFirefoxAccount",
"Accounts:Exist",
"Accounts:ProfileUpdated",
"Accounts:ShowSyncPreferences");
}
public synchronized void uninit() {
EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
if (dispatcher == null) {
Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
return;
}
dispatcher.unregisterGeckoThreadListener(this,
"Accounts:CreateFirefoxAccountFromJSON",
"Accounts:UpdateFirefoxAccountFromJSON",
"Accounts:Create",
"Accounts:DeleteFirefoxAccount",
"Accounts:Exist",
"Accounts:ProfileUpdated",
"Accounts:ShowSyncPreferences");
}
@Override
public void handleMessage(String event, NativeJSObject message, final EventCallback callback) {
if (!Restrictions.isAllowed(mContext, Restrictable.MODIFY_ACCOUNTS)) {
// We register for messages in all contexts; we drop, with a log and an error to JavaScript,
// when the profile is restricted. It's better to return errors than silently ignore messages.
Log.e(LOGTAG, "Profile is not allowed to modify accounts! Ignoring event: " + event);
if (callback != null) {
callback.sendError("Profile is not allowed to modify accounts!");
}
return;
}
if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) {
// As we are about to create a new account, let's ensure our in-memory accounts cache
// is empty so that there are no undesired side-effects.
AndroidFxAccount.invalidateCaches();
AndroidFxAccount fxAccount = null;
try {
final NativeJSObject json = message.getObject("json");
final String email = json.getString("email");
final String uid = json.getString("uid");
final boolean verified = json.optBoolean("verified", false);
final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
final String authServerEndpoint =
json.optString("authServerEndpoint", FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT);
final String tokenServerEndpoint =
json.optString("tokenServerEndpoint", FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT);
final String profileServerEndpoint =
json.optString("profileServerEndpoint", FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
// TODO: handle choose what to Sync.
State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
fxAccount = AndroidFxAccount.addAndroidAccount(mContext,
email,
mProfile.getName(),
authServerEndpoint,
tokenServerEndpoint,
profileServerEndpoint,
state,
AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
final String[] declinedSyncEngines = json.optStringArray("declinedSyncEngines", null);
if (declinedSyncEngines != null) {
Log.i(LOGTAG, "User has selected engines; storing to prefs.");
final Map<String, Boolean> selectedEngines = new HashMap<String, Boolean>();
for (String enabledSyncEngine : SyncConfiguration.validEngineNames()) {
selectedEngines.put(enabledSyncEngine, true);
}
for (String declinedSyncEngine : declinedSyncEngines) {
selectedEngines.put(declinedSyncEngine, false);
}
// The "forms" engine has the same state as the "history" engine.
selectedEngines.put("forms", selectedEngines.get("history"));
FxAccountUtils.pii(LOGTAG, "User selected engines: " + selectedEngines.toString());
try {
SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines);
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
Log.e(LOGTAG, "Got exception storing selected engines; ignoring.", e);
}
}
} catch (URISyntaxException | GeneralSecurityException | UnsupportedEncodingException e) {
Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
if (callback != null) {
callback.sendError("Could not create Firefox Account from JSON: " + e.toString());
return;
}
}
if (callback != null) {
callback.sendSuccess(fxAccount != null);
}
} else if ("Accounts:UpdateFirefoxAccountFromJSON".equals(event)) {
// We might be significantly changing state of the account; let's ensure our in-memory
// accounts cache is empty so that there are no undesired side-effects.
AndroidFxAccount.invalidateCaches();
try {
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
if (account == null) {
if (callback != null) {
callback.sendError("Could not update Firefox Account since none exists");
}
return;
}
final NativeJSObject json = message.getObject("json");
final String email = json.getString("email");
final String uid = json.getString("uid");
// Protect against cross-connecting accounts.
if (account.name == null || !account.name.equals(email)) {
final String errorMessage = "Cannot update Firefox Account from JSON: datum has different email address!";
Log.e(LOGTAG, errorMessage);
if (callback != null) {
callback.sendError(errorMessage);
}
return;
}
final boolean verified = json.optBoolean("verified", false);
final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
final State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
fxAccount.setState(state);
if (callback != null) {
callback.sendSuccess(true);
}
} catch (NativeJSObject.InvalidPropertyException e) {
Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
if (callback != null) {
callback.sendError("Could not update Firefox Account from JSON: " + e.toString());
return;
}
}
} else if ("Accounts:Create".equals(event)) {
// Do exactly the same thing as if you tapped 'Sync' in Settings.
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final NativeJSObject extras = message.optObject("extras", null);
if (extras != null) {
intent.putExtra("extras", extras.toString());
}
mContext.startActivity(intent);
} else if ("Accounts:DeleteFirefoxAccount".equals(event)) {
try {
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
if (account == null) {
Log.w(LOGTAG, "Could not delete Firefox Account since none exists!");
if (callback != null) {
callback.sendError("Could not delete Firefox Account since none exists");
}
return;
}
final AccountManagerCallback<Boolean> accountManagerCallback = new AccountManagerCallback<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> future) {
try {
final boolean result = future.getResult();
Log.i(LOGTAG, "Account named like " + Utils.obfuscateEmail(account.name) + " removed: " + result);
if (callback != null) {
callback.sendSuccess(result);
}
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
if (callback != null) {
callback.sendError("Could not delete Firefox Account: " + e.toString());
}
}
}
};
AccountManager.get(mContext).removeAccount(account, accountManagerCallback, null);
} catch (Exception e) {
Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
if (callback != null) {
callback.sendError("Could not update Firefox Account from JSON: " + e.toString());
return;
}
}
} else if ("Accounts:Exist".equals(event)) {
if (callback == null) {
Log.w(LOGTAG, "Accounts:Exist requires a callback");
return;
}
final String kind = message.optString("kind", null);
final JSONObject response = new JSONObject();
try {
if ("any".equals(kind)) {
response.put("exists", FirefoxAccounts.firefoxAccountsExist(mContext));
callback.sendSuccess(response);
} else if ("fxa".equals(kind)) {
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
response.put("exists", account != null);
if (account != null) {
response.put("email", account.name);
// We should always be able to extract the server endpoints.
final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
response.put("authServerEndpoint", fxAccount.getAccountServerURI());
response.put("profileServerEndpoint", fxAccount.getProfileServerURI());
response.put("tokenServerEndpoint", fxAccount.getTokenServerURI());
try {
// It is possible for the state fetch to fail and us to not be able to provide a UID.
// Long term, the UID (and verification flag) will be attached to the Android account
// user data and not the internal state representation.
final State state = fxAccount.getState();
response.put("uid", state.uid);
} catch (Exception e) {
Log.w(LOGTAG, "Got exception extracting account UID; ignoring.", e);
}
}
callback.sendSuccess(response);
} else {
callback.sendError("Could not query account existence: unknown kind.");
}
} catch (JSONException e) {
Log.w(LOGTAG, "Got exception querying account existence; ignoring.", e);
callback.sendError("Could not query account existence: " + e.toString());
return;
}
} else if ("Accounts:ProfileUpdated".equals(event)) {
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
if (account == null) {
Log.w(LOGTAG, "Can't change profile of non-existent Firefox Account!; ignored");
return;
}
final AndroidFxAccount androidFxAccount = new AndroidFxAccount(mContext, account);
androidFxAccount.fetchProfileJSON();
} else if ("Accounts:ShowSyncPreferences".equals(event)) {
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
if (account == null) {
Log.w(LOGTAG, "Can't change show Sync preferences of non-existent Firefox Account!; ignored");
return;
}
// We don't necessarily have an Activity context here, so we always start in a new task.
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
}
}

View file

@ -0,0 +1,256 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuItem;
import org.mozilla.gecko.util.ResourceDrawableUtils;
import org.mozilla.gecko.text.TextSelection;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.ActionModeCompat.Callback;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.MenuItem;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Timer;
import java.util.TimerTask;
import android.util.Log;
class ActionBarTextSelection implements TextSelection, GeckoEventListener {
private static final String LOGTAG = "GeckoTextSelection";
private static final int SHUTDOWN_DELAY_MS = 250;
private final Context context;
private boolean mDraggingHandles;
private String selectionID; // Unique ID provided for each selection action.
private String mCurrentItems;
private TextSelectionActionModeCallback mCallback;
// These timers are used to avoid flicker caused by selection handles showing/hiding quickly.
// For instance when moving between single handle caret mode and two handle selection mode.
private final Timer mActionModeTimer = new Timer("actionMode");
private class ActionModeTimerTask extends TimerTask {
@Override
public void run() {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
endActionMode();
}
});
}
};
private ActionModeTimerTask mActionModeTimerTask;
ActionBarTextSelection(Context context) {
this.context = context;
}
@Override
public void create() {
// Only register listeners if we have valid start/middle/end handles
if (context == null) {
Log.e(LOGTAG, "Failed to initialize text selection because at least one context is null");
} else {
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"TextSelection:ActionbarInit",
"TextSelection:ActionbarStatus",
"TextSelection:ActionbarUninit",
"TextSelection:Update");
}
}
@Override
public boolean dismiss() {
// We do not call endActionMode() here because this is already handled by the activity.
return false;
}
@Override
public void destroy() {
if (context == null) {
Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since context is null");
} else {
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"TextSelection:ActionbarInit",
"TextSelection:ActionbarStatus",
"TextSelection:ActionbarUninit",
"TextSelection:Update");
}
}
@Override
public void handleMessage(final String event, final JSONObject message) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
try {
if (event.equals("TextSelection:Update")) {
if (mActionModeTimerTask != null)
mActionModeTimerTask.cancel();
showActionMode(message.getJSONArray("actions"));
} else if (event.equals("TextSelection:ActionbarInit")) {
// Init / Open the action bar. Note the current selectionID,
// cancel any pending actionBar close.
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
TelemetryContract.Method.CONTENT, "text_selection");
selectionID = message.getString("selectionID");
mCurrentItems = null;
if (mActionModeTimerTask != null) {
mActionModeTimerTask.cancel();
}
} else if (event.equals("TextSelection:ActionbarStatus")) {
// Ensure async updates from SearchService for example are valid.
if (selectionID != message.optString("selectionID")) {
return;
}
// Update the actionBar actions as provided by Gecko.
showActionMode(message.getJSONArray("actions"));
} else if (event.equals("TextSelection:ActionbarUninit")) {
// Uninit the actionbar. Schedule a cancellable close
// action to avoid UI jank. (During SelectionAll for ex).
mCurrentItems = null;
mActionModeTimerTask = new ActionModeTimerTask();
mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS);
}
} catch (JSONException e) {
Log.e(LOGTAG, "JSON exception", e);
}
}
});
}
private void showActionMode(final JSONArray items) {
String itemsString = items.toString();
if (itemsString.equals(mCurrentItems)) {
return;
}
mCurrentItems = itemsString;
if (mCallback != null) {
mCallback.updateItems(items);
return;
}
if (context instanceof ActionModeCompat.Presenter) {
final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
mCallback = new TextSelectionActionModeCallback(items);
presenter.startActionModeCompat(mCallback);
mCallback.animateIn();
}
}
private void endActionMode() {
if (context instanceof ActionModeCompat.Presenter) {
final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
presenter.endActionModeCompat();
}
mCurrentItems = null;
}
private class TextSelectionActionModeCallback implements Callback {
private JSONArray mItems;
private ActionModeCompat mActionMode;
public TextSelectionActionModeCallback(JSONArray items) {
mItems = items;
}
public void updateItems(JSONArray items) {
mItems = items;
if (mActionMode != null) {
mActionMode.invalidate();
}
}
public void animateIn() {
if (mActionMode != null) {
mActionMode.animateIn();
}
}
@Override
public boolean onPrepareActionMode(final ActionModeCompat mode, final GeckoMenu menu) {
// Android would normally expect us to only update the state of menu items here
// To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all
// the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the
// action mode.
menu.clear();
int length = mItems.length();
for (int i = 0; i < length; i++) {
try {
final JSONObject obj = mItems.getJSONObject(i);
final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER;
menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle);
final String iconString = obj.optString("icon");
ResourceDrawableUtils.getDrawable(context, iconString, new ResourceDrawableUtils.BitmapLoader() {
@Override
public void onBitmapFound(Drawable d) {
if (d != null) {
menuitem.setIcon(d);
}
}
});
} catch (Exception ex) {
Log.i(LOGTAG, "Exception building menu", ex);
}
}
return true;
}
@Override
public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu unused) {
mActionMode = mode;
return true;
}
@Override
public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
try {
final JSONObject obj = mItems.getJSONObject(item.getItemId());
GeckoAppShell.notifyObservers("TextSelection:Action", obj.optString("id"));
return true;
} catch (Exception ex) {
Log.i(LOGTAG, "Exception calling action", ex);
}
return false;
}
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionModeCompat mode) {
mActionMode = null;
mCallback = null;
final JSONObject args = new JSONObject();
try {
args.put("selectionID", selectionID);
} catch (JSONException e) {
Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e);
return;
}
GeckoAppShell.notifyObservers("TextSelection:End", args.toString());
}
}
}

View file

@ -0,0 +1,135 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuItem;
import org.mozilla.gecko.widget.GeckoPopupMenu;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
class ActionModeCompat implements GeckoPopupMenu.OnMenuItemClickListener,
GeckoPopupMenu.OnMenuItemLongClickListener,
View.OnClickListener {
private final String LOGTAG = "GeckoActionModeCompat";
private final Callback mCallback;
private final ActionModeCompatView mView;
private final Presenter mPresenter;
/* A set of callbacks to be called during this ActionMode's lifecycle. These will control the
* creation, interaction with, and destruction of menuitems for the view */
public static interface Callback {
/* Called when action mode is first created. Implementors should use this to inflate menu resources. */
public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu menu);
/* Called to refresh an action mode's action menu. Called whenever the mode is invalidated. Implementors
* should use this to enable/disable/show/hide menu items. */
public boolean onPrepareActionMode(ActionModeCompat mode, GeckoMenu menu);
/* Called to report a user click on an action button. */
public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item);
/* Called when an action mode is about to be exited and destroyed. */
public void onDestroyActionMode(ActionModeCompat mode);
}
/* Presenters handle the actual showing/hiding of the action mode UI in the app. Its their responsibility
* to create an action mode, and assign it Callbacks and ActionModeCompatView's. */
public static interface Presenter {
/* Called when an action mode should be shown */
public void startActionModeCompat(final Callback callback);
/* Called when whatever action mode is showing should be hidden */
public void endActionModeCompat();
}
public ActionModeCompat(Presenter presenter, Callback callback, ActionModeCompatView view) {
mPresenter = presenter;
mCallback = callback;
mView = view;
mView.initForMode(this);
}
public void finish() {
// Clearing the menu will also clear the ActionItemBar
final GeckoMenu menu = mView.getMenu();
menu.clear();
menu.close();
if (mCallback != null) {
mCallback.onDestroyActionMode(this);
}
}
public CharSequence getTitle() {
return mView.getTitle();
}
public void setTitle(CharSequence title) {
mView.setTitle(title);
}
public void setTitle(int resId) {
mView.setTitle(resId);
}
public GeckoMenu getMenu() {
return mView.getMenu();
}
public void invalidate() {
if (mCallback != null) {
mCallback.onPrepareActionMode(this, mView.getMenu());
}
mView.invalidate();
}
public void animateIn() {
mView.animateIn();
}
/* GeckoPopupMenu.OnMenuItemClickListener */
@Override
public boolean onMenuItemClick(MenuItem item) {
if (mCallback != null) {
return mCallback.onActionItemClicked(this, item);
}
return false;
}
/* GeckoPopupMenu.onMenuItemLongClickListener */
@Override
public boolean onMenuItemLongClick(MenuItem item) {
showTooltip((GeckoMenuItem) item);
return true;
}
/* View.OnClickListener*/
@Override
public void onClick(View v) {
mPresenter.endActionModeCompat();
}
private void showTooltip(GeckoMenuItem item) {
// Computes the tooltip toast screen position (shown when long-tapping the menu item) with regards to the
// menu item's position (i.e below the item and slightly to the left)
int[] location = new int[2];
final View view = item.getActionView();
view.getLocationOnScreen(location);
int xOffset = location[0] - view.getWidth();
int yOffset = location[1] + view.getHeight() / 2;
Toast toast = Toast.makeText(view.getContext(), item.getTitle(), Toast.LENGTH_SHORT);
toast.setGravity(Gravity.TOP | Gravity.LEFT, xOffset, yOffset);
toast.show();
}
}

View file

@ -0,0 +1,202 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import org.mozilla.gecko.animation.AnimationUtils;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.widget.GeckoPopupMenu;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
class ActionModeCompatView extends LinearLayout implements GeckoMenu.ActionItemBarPresenter {
private final String LOGTAG = "GeckoActionModeCompatPresenter";
private static final int SPEC = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
private Button mTitleView;
private ImageButton mMenuButton;
private ViewGroup mActionButtonBar;
private GeckoPopupMenu mPopupMenu;
// Maximum number of items to show as actions
private static final int MAX_ACTION_ITEMS = 4;
private int mActionButtonsWidth;
private Paint mBottomDividerPaint;
private int mBottomDividerOffset;
public ActionModeCompatView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public ActionModeCompatView(Context context, AttributeSet attrs, int style) {
super(context, attrs, style);
init(context, attrs, style);
}
public void init(final Context context, final AttributeSet attrs, final int defStyle) {
LayoutInflater.from(context).inflate(R.layout.actionbar, this);
mTitleView = (Button) findViewById(R.id.actionmode_title);
mMenuButton = (ImageButton) findViewById(R.id.actionbar_menu);
mActionButtonBar = (ViewGroup) findViewById(R.id.actionbar_buttons);
mPopupMenu = new GeckoPopupMenu(getContext(), mMenuButton);
mPopupMenu.getMenu().setActionItemBarPresenter(this);
mMenuButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openMenu();
}
});
// The built-in action bar uses colorAccent for the divider so we duplicate that here.
final TypedArray arr = context.obtainStyledAttributes(attrs, new int[] { R.attr.colorAccent }, defStyle, 0);
final int bottomDividerColor = arr.getColor(0, 0);
arr.recycle();
mBottomDividerPaint = new Paint();
mBottomDividerPaint.setColor(bottomDividerColor);
mBottomDividerOffset = getResources().getDimensionPixelSize(R.dimen.action_bar_divider_height);
}
public void initForMode(final ActionModeCompat mode) {
mTitleView.setOnClickListener(mode);
mPopupMenu.setOnMenuItemClickListener(mode);
mPopupMenu.setOnMenuItemLongClickListener(mode);
}
public CharSequence getTitle() {
return mTitleView.getText();
}
public void setTitle(CharSequence title) {
mTitleView.setText(title);
}
public void setTitle(int resId) {
mTitleView.setText(resId);
}
public GeckoMenu getMenu() {
return mPopupMenu.getMenu();
}
@Override
public void invalidate() {
// onFinishInflate may not have been called yet on some versions of Android
if (mPopupMenu != null && mMenuButton != null) {
mMenuButton.setVisibility(mPopupMenu.getMenu().hasVisibleItems() ? View.VISIBLE : View.GONE);
}
super.invalidate();
}
/* GeckoMenu.ActionItemBarPresenter */
@Override
public boolean addActionItem(View actionItem) {
final int count = mActionButtonBar.getChildCount();
if (count >= MAX_ACTION_ITEMS) {
return false;
}
int maxWidth = mActionButtonBar.getMeasuredWidth();
if (maxWidth == 0) {
mActionButtonBar.measure(SPEC, SPEC);
maxWidth = mActionButtonBar.getMeasuredWidth();
}
// If the menu button is already visible, no need to account for it
if (mMenuButton.getVisibility() == View.GONE) {
// Since we don't know how many items will be added, we always reserve space for the overflow menu
mMenuButton.measure(SPEC, SPEC);
maxWidth -= mMenuButton.getMeasuredWidth();
}
if (mActionButtonsWidth <= 0) {
mActionButtonsWidth = 0;
// Loop over child views, measure them, and add their width to the taken width
for (int i = 0; i < count; i++) {
View v = mActionButtonBar.getChildAt(i);
v.measure(SPEC, SPEC);
mActionButtonsWidth += v.getMeasuredWidth();
}
}
actionItem.measure(SPEC, SPEC);
int w = actionItem.getMeasuredWidth();
if (mActionButtonsWidth + w < maxWidth) {
// We cache the new width of our children.
mActionButtonsWidth += w;
mActionButtonBar.addView(actionItem);
return true;
}
return false;
}
/* GeckoMenu.ActionItemBarPresenter */
@Override
public void removeActionItem(View actionItem) {
actionItem.measure(SPEC, SPEC);
mActionButtonsWidth -= actionItem.getMeasuredWidth();
mActionButtonBar.removeView(actionItem);
}
public void openMenu() {
mPopupMenu.openMenu();
}
public void closeMenu() {
mPopupMenu.dismiss();
}
public void animateIn() {
long duration = AnimationUtils.getShortDuration(getContext());
TranslateAnimation t = new TranslateAnimation(Animation.RELATIVE_TO_SELF, -0.5f, Animation.RELATIVE_TO_SELF, 0f,
Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f);
t.setDuration(duration);
ScaleAnimation s = new ScaleAnimation(1f, 1f, 0f, 1f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
s.setDuration((long) (duration * 1.5f));
mTitleView.startAnimation(t);
mActionButtonBar.startAnimation(s);
if ((mMenuButton.getVisibility() == View.VISIBLE) &&
(mPopupMenu.getMenu().size() > 0)) {
mMenuButton.startAnimation(s);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the divider at the bottom of the screen. We could do this with a layer-list
// but then we'd have overdraw (http://stackoverflow.com/a/13509472).
final int bottom = getHeight();
final int top = bottom - mBottomDividerOffset;
canvas.drawRect(0, top, getWidth(), bottom, mBottomDividerPaint);
}
}

View file

@ -0,0 +1,61 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.ActivityResultHandlerMap;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class ActivityHandlerHelper {
private static final String LOGTAG = "GeckoActivityHandlerHelper";
private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
private static int makeRequestCode(ActivityResultHandler aHandler) {
return mActivityResultHandlerMap.put(aHandler);
}
public static void startIntent(Intent intent, ActivityResultHandler activityResultHandler) {
startIntentForActivity(GeckoAppShell.getGeckoInterface().getActivity(), intent, activityResultHandler);
}
/**
* Starts the Activity, catching & logging if the Activity fails to start.
*
* We catch to prevent callers from passing in invalid Intents and crashing the browser.
*
* @return true if the Activity is successfully started, false otherwise.
*/
public static boolean startIntentAndCatch(final String logtag, final Context context, final Intent intent) {
try {
context.startActivity(intent);
return true;
} catch (final ActivityNotFoundException e) {
Log.w(logtag, "Activity not found.", e);
return false;
} catch (final SecurityException e) {
Log.w(logtag, "Forbidden to launch activity.", e);
return false;
}
}
public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
}
public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
if (handler != null) {
handler.onActivityResult(resultCode, data);
return true;
}
return false;
}
}

View file

@ -0,0 +1,27 @@
/* -*- 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;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.mozilla.gecko.feeds.FeedService;
/**
* This broadcast receiver receives ACTION_BOOT_COMPLETED broadcasts and starts components that should
* run after the device has booted.
*/
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || !intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
return; // This is not the broadcast you are looking for.
}
FeedService.setup(context);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,439 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.io.File;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.ReflectionTarget;
import org.mozilla.gecko.util.GeckoJarReader;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
/**
* This class manages persistence, application, and otherwise handling of
* user-specified locales.
*
* Of note:
*
* * It's a singleton, because its scope extends to that of the application,
* and definitionally all changes to the locale of the app must go through
* this.
* * It's lazy.
* * It has ties into the Gecko event system, because it has to tell Gecko when
* to switch locale.
* * It relies on using the SharedPreferences file owned by the browser (in
* Fennec's case, "GeckoApp") for performance.
*/
public class BrowserLocaleManager implements LocaleManager {
private static final String LOG_TAG = "GeckoLocales";
private static final String EVENT_LOCALE_CHANGED = "Locale:Changed";
private static final String PREF_LOCALE = "locale";
private static final String FALLBACK_LOCALE_TAG = "en-US";
// These are volatile because we don't impose restrictions
// over which thread calls our methods.
private volatile Locale currentLocale;
private volatile Locale systemLocale = Locale.getDefault();
private final AtomicBoolean inited = new AtomicBoolean(false);
private boolean systemLocaleDidChange;
private BroadcastReceiver receiver;
private static final AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>();
@ReflectionTarget
public static LocaleManager getInstance() {
LocaleManager localeManager = instance.get();
if (localeManager != null) {
return localeManager;
}
localeManager = new BrowserLocaleManager();
if (instance.compareAndSet(null, localeManager)) {
return localeManager;
} else {
return instance.get();
}
}
@Override
public boolean isEnabled() {
return AppConstants.MOZ_LOCALE_SWITCHER;
}
/**
* Ensure that you call this early in your application startup,
* and with a context that's sufficiently long-lived (typically
* the application context).
*
* Calling multiple times is harmless.
*/
@Override
public void initialize(final Context context) {
if (!inited.compareAndSet(false, true)) {
return;
}
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final Locale current = systemLocale;
// We don't trust Locale.getDefault() here, because we make a
// habit of mutating it! Use the one Android supplies, because
// that gets regularly reset.
// The default value of systemLocale is fine, because we haven't
// yet swizzled Locale during static initialization.
systemLocale = context.getResources().getConfiguration().locale;
systemLocaleDidChange = true;
Log.d(LOG_TAG, "System locale changed from " + current + " to " + systemLocale);
}
};
context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
}
@Override
public boolean systemLocaleDidChange() {
return systemLocaleDidChange;
}
/**
* Every time the system gives us a new configuration, it
* carries the external locale. Fix it.
*/
@Override
public void correctLocale(Context context, Resources res, Configuration config) {
final Locale current = getCurrentLocale(context);
if (current == null) {
Log.d(LOG_TAG, "No selected locale. No correction needed.");
return;
}
// I know it's tempting to short-circuit here if the config seems to be
// up-to-date, but the rest is necessary.
config.locale = current;
// The following two lines are heavily commented in case someone
// decides to chase down performance improvements and decides to
// question what's going on here.
// Both lines should be cheap, *but*...
// This is unnecessary for basic string choice, but it almost
// certainly comes into play when rendering numbers, deciding on RTL,
// etc. Take it out if you can prove that's not the case.
Locale.setDefault(current);
// This seems to be a no-op, but every piece of documentation under the
// sun suggests that it's necessary, and it certainly makes sense.
res.updateConfiguration(config, null);
}
/**
* We can be in one of two states.
*
* If the user has not explicitly chosen a Firefox-specific locale, we say
* we are "mirroring" the system locale.
*
* When we are not mirroring, system locale changes do not impact Firefox
* and are essentially ignored; the user's locale selection is the only
* thing we care about, and we actively correct incoming configuration
* changes to reflect the user's chosen locale.
*
* By contrast, when we are mirroring, system locale changes cause Firefox
* to reflect the new system locale, as if the user picked the new locale.
*
* If we're currently mirroring the system locale, this method returns the
* supplied configuration's locale, unless the current activity locale is
* correct. If we're not currently mirroring, this method updates the
* configuration object to match the user's currently selected locale, and
* returns that, unless the current activity locale is correct.
*
* If the current activity locale is correct, returns null.
*
* The caller is expected to redisplay themselves accordingly.
*
* This method is intended to be called from inside
* <code>onConfigurationChanged(Configuration)</code> as part of a strategy
* to detect and either apply or undo system locale changes.
*/
@Override
public Locale onSystemConfigurationChanged(final Context context, final Resources resources, final Configuration configuration, final Locale currentActivityLocale) {
if (!isMirroringSystemLocale(context)) {
correctLocale(context, resources, configuration);
}
final Locale changed = configuration.locale;
if (changed.equals(currentActivityLocale)) {
return null;
}
return changed;
}
/**
* Gecko needs to know the OS locale to compute a useful Accept-Language
* header. If it changed since last time, send a message to Gecko and
* persist the new value. If unchanged, returns immediately.
*
* @param prefs the SharedPreferences instance to use. Cannot be null.
* @param osLocale the new locale instance. Safe if null.
*/
public static void storeAndNotifyOSLocale(final SharedPreferences prefs,
final Locale osLocale) {
if (osLocale == null) {
return;
}
final String lastOSLocale = prefs.getString("osLocale", null);
final String osLocaleString = osLocale.toString();
if (osLocaleString.equals(lastOSLocale)) {
return;
}
// Store the Java-native form.
prefs.edit().putString("osLocale", osLocaleString).apply();
// The value we send to Gecko should be a language tag, not
// a Java locale string.
final String osLanguageTag = Locales.getLanguageTag(osLocale);
GeckoAppShell.notifyObservers("Locale:OS", osLanguageTag);
}
@Override
public String getAndApplyPersistedLocale(Context context) {
initialize(context);
final long t1 = android.os.SystemClock.uptimeMillis();
final String localeCode = getPersistedLocale(context);
if (localeCode == null) {
return null;
}
// Note that we don't tell Gecko about this. We notify Gecko when the
// locale is set, not when we update Java.
final String resultant = updateLocale(context, localeCode);
if (resultant == null) {
// Update the configuration anyway.
updateConfiguration(context, currentLocale);
}
final long t2 = android.os.SystemClock.uptimeMillis();
Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms.");
return resultant;
}
/**
* Returns the set locale if it changed.
*
* Always persists and notifies Gecko.
*/
@Override
public String setSelectedLocale(Context context, String localeCode) {
final String resultant = updateLocale(context, localeCode);
// We always persist and notify Gecko, even if nothing seemed to
// change. This might happen if you're picking a locale that's the same
// as the current OS locale. The OS locale might change next time we
// launch, and we need the Gecko pref and persisted locale to have been
// set by the time that happens.
persistLocale(context, localeCode);
// Tell Gecko.
GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context)));
return resultant;
}
@Override
public void resetToSystemLocale(Context context) {
// Wipe the pref.
final SharedPreferences settings = getSharedPreferences(context);
settings.edit().remove(PREF_LOCALE).apply();
// Apply the system locale.
updateLocale(context, systemLocale);
// Tell Gecko.
GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, "");
}
/**
* This is public to allow for an activity to force the
* current locale to be applied if necessary (e.g., when
* a new activity launches).
*/
@Override
public void updateConfiguration(Context context, Locale locale) {
Resources res = context.getResources();
Configuration config = res.getConfiguration();
// We should use setLocale, but it's unexpectedly missing
// on real devices.
config.locale = locale;
res.updateConfiguration(config, null);
}
private SharedPreferences getSharedPreferences(Context context) {
return GeckoSharedPrefs.forApp(context);
}
/**
* @return the persisted locale in Java format: "en_US".
*/
private String getPersistedLocale(Context context) {
final SharedPreferences settings = getSharedPreferences(context);
final String locale = settings.getString(PREF_LOCALE, "");
if ("".equals(locale)) {
return null;
}
return locale;
}
private void persistLocale(Context context, String localeCode) {
final SharedPreferences settings = getSharedPreferences(context);
settings.edit().putString(PREF_LOCALE, localeCode).apply();
}
@Override
public Locale getCurrentLocale(Context context) {
if (currentLocale != null) {
return currentLocale;
}
final String current = getPersistedLocale(context);
if (current == null) {
return null;
}
return currentLocale = Locales.parseLocaleCode(current);
}
/**
* Updates the Java locale and the Android configuration.
*
* Returns the persisted locale if it differed.
*
* Does not notify Gecko.
*
* @param localeCode a locale string in Java format: "en_US".
* @return if it differed, a locale string in Java format: "en_US".
*/
private String updateLocale(Context context, String localeCode) {
// Fast path.
final Locale defaultLocale = Locale.getDefault();
if (defaultLocale.toString().equals(localeCode)) {
return null;
}
final Locale locale = Locales.parseLocaleCode(localeCode);
return updateLocale(context, locale);
}
/**
* @return the Java locale string: e.g., "en_US".
*/
private String updateLocale(Context context, final Locale locale) {
// Fast path.
if (Locale.getDefault().equals(locale)) {
return null;
}
Locale.setDefault(locale);
currentLocale = locale;
// Update resources.
updateConfiguration(context, locale);
return locale.toString();
}
private boolean isMirroringSystemLocale(final Context context) {
return getPersistedLocale(context) == null;
}
/**
* Examines <code>multilocale.json</code>, returning the included list of
* locale codes.
*
* If <code>multilocale.json</code> is not present, returns
* <code>null</code>. In that case, consider {@link #getFallbackLocaleTag()}.
*
* multilocale.json currently looks like this:
*
* <code>
* {"locales": ["en-US", "be", "ca", "cs", "da", "de", "en-GB",
* "en-ZA", "es-AR", "es-ES", "es-MX", "et", "fi",
* "fr", "ga-IE", "hu", "id", "it", "ja", "ko",
* "lt", "lv", "nb-NO", "nl", "pl", "pt-BR",
* "pt-PT", "ro", "ru", "sk", "sl", "sv-SE", "th",
* "tr", "uk", "zh-CN", "zh-TW", "en-US"]}
* </code>
*/
public static Collection<String> getPackagedLocaleTags(final Context context) {
final String resPath = "res/multilocale.json";
final String jarURL = GeckoJarReader.getJarURL(context, resPath);
final String contents = GeckoJarReader.getText(context, jarURL);
if (contents == null) {
// GeckoJarReader logs and swallows exceptions.
return null;
}
try {
final JSONObject multilocale = new JSONObject(contents);
final JSONArray locales = multilocale.getJSONArray("locales");
if (locales == null) {
Log.e(LOG_TAG, "No 'locales' array in multilocales.json!");
return null;
}
final Set<String> out = new HashSet<String>(locales.length());
for (int i = 0; i < locales.length(); ++i) {
// If any item in the array is invalid, this will throw,
// and the entire clause will fail, being caught below
// and returning null.
out.add(locales.getString(i));
}
return out;
} catch (JSONException e) {
Log.e(LOG_TAG, "Unable to parse multilocale.json.", e);
return null;
}
}
/**
* @return the single default locale baked into this application.
* Applicable when there is no multilocale.json present.
*/
@SuppressWarnings("static-method")
public String getFallbackLocaleTag() {
return FALLBACK_LOCALE_TAG;
}
}

View file

@ -0,0 +1,112 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.json.JSONObject;
import org.json.JSONException;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.EventCallback;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastRemoteDisplayLocalService;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.Status;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
public class ChromeCastDisplay implements GeckoPresentationDisplay {
static final String REMOTE_DISPLAY_APP_ID = "4574A331";
private static final String LOGTAG = "GeckoChromeCastDisplay";
private final Context context;
private final RouteInfo route;
private CastDevice castDevice;
public ChromeCastDisplay(Context context, RouteInfo route) {
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
if (status != ConnectionResult.SUCCESS) {
throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
}
this.context = context;
this.route = route;
this.castDevice = CastDevice.getFromBundle(route.getExtras());
}
public JSONObject toJSON() {
final JSONObject obj = new JSONObject();
try {
if (castDevice == null) {
return null;
}
obj.put("uuid", route.getId());
obj.put("friendlyName", castDevice.getFriendlyName());
obj.put("type", "chromecast");
} catch (JSONException ex) {
Log.d(LOGTAG, "Error building route", ex);
}
return obj;
}
@Override
public void start(final EventCallback callback) {
if (CastRemoteDisplayLocalService.getInstance() != null) {
Log.d(LOGTAG, "CastRemoteDisplayLocalService already existed.");
GeckoAppShell.notifyObservers("presentation-view-ready", route.getId());
callback.sendSuccess("Succeed to start presentation.");
return;
}
Intent intent = new Intent(context, RemotePresentationService.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent notificationPendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
CastRemoteDisplayLocalService.NotificationSettings settings =
new CastRemoteDisplayLocalService.NotificationSettings.Builder()
.setNotificationPendingIntent(notificationPendingIntent).build();
CastRemoteDisplayLocalService.startService(
context,
RemotePresentationService.class,
REMOTE_DISPLAY_APP_ID,
castDevice,
settings,
new CastRemoteDisplayLocalService.Callbacks() {
@Override
public void onServiceCreated(CastRemoteDisplayLocalService service) {
((RemotePresentationService) service).setDeviceId(route.getId());
}
@Override
public void onRemoteDisplaySessionStarted(CastRemoteDisplayLocalService service) {
Log.d(LOGTAG, "Remote presentation launched!");
callback.sendSuccess("Succeed to start presentation.");
}
@Override
public void onRemoteDisplaySessionError(Status errorReason) {
int code = errorReason.getStatusCode();
callback.sendError("Fail to start presentation. Error code: " + code);
}
});
}
@Override
public void stop(EventCallback callback) {
CastRemoteDisplayLocalService.stopService();
callback.sendSuccess("Succeed to stop presentation.");
}
}

View file

@ -0,0 +1,509 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.io.IOException;
import org.mozilla.gecko.util.EventCallback;
import org.json.JSONObject;
import org.json.JSONException;
import com.google.android.gms.cast.Cast.MessageReceivedCallback;
import com.google.android.gms.cast.ApplicationMetadata;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.RemoteMediaPlayer;
import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.common.GooglePlayServicesUtil;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
/* Implementation of GeckoMediaPlayer for talking to ChromeCast devices */
class ChromeCastPlayer implements GeckoMediaPlayer {
private static final boolean SHOW_DEBUG = false;
static final String MIRROR_RECEIVER_APP_ID = "08FF1091";
private final Context context;
private final RouteInfo route;
private GoogleApiClient apiClient;
private RemoteMediaPlayer remoteMediaPlayer;
private final boolean canMirror;
private String mSessionId;
private MirrorChannel mMirrorChannel;
private boolean mApplicationStarted = false;
// EventCallback which is actually a GeckoEventCallback is sometimes being invoked more
// than once. That causes the IllegalStateException to be thrown. To prevent a crash,
// catch the exception and report it as an error to the log.
private static void sendSuccess(final EventCallback callback, final String msg) {
try {
callback.sendSuccess(msg);
} catch (final IllegalStateException e) {
Log.e(LOGTAG, "Attempting to invoke callback.sendSuccess more than once.", e);
}
}
private static void sendError(final EventCallback callback, final String msg) {
try {
callback.sendError(msg);
} catch (final IllegalStateException e) {
Log.e(LOGTAG, "Attempting to invoke callback.sendError more than once.", e);
}
}
// Callback to start playback of a url on a remote device
private class VideoPlayCallback implements ResultCallback<ApplicationConnectionResult>,
RemoteMediaPlayer.OnStatusUpdatedListener,
RemoteMediaPlayer.OnMetadataUpdatedListener {
private final String url;
private final String type;
private final String title;
private final EventCallback callback;
public VideoPlayCallback(String url, String type, String title, EventCallback callback) {
this.url = url;
this.type = type;
this.title = title;
this.callback = callback;
}
@Override
public void onStatusUpdated() {
MediaStatus mediaStatus = remoteMediaPlayer.getMediaStatus();
switch (mediaStatus.getPlayerState()) {
case MediaStatus.PLAYER_STATE_PLAYING:
GeckoAppShell.notifyObservers("MediaPlayer:Playing", null);
break;
case MediaStatus.PLAYER_STATE_PAUSED:
GeckoAppShell.notifyObservers("MediaPlayer:Paused", null);
break;
case MediaStatus.PLAYER_STATE_IDLE:
// TODO: Do we want to shutdown when there are errors?
if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
GeckoAppShell.notifyObservers("Casting:Stop", null);
}
break;
default:
// TODO: Do we need to handle other status such as buffering / unknown?
break;
}
}
@Override
public void onMetadataUpdated() { }
@Override
public void onResult(ApplicationConnectionResult result) {
Status status = result.getStatus();
debug("ApplicationConnectionResultCallback.onResult: statusCode" + status.getStatusCode());
if (status.isSuccess()) {
remoteMediaPlayer = new RemoteMediaPlayer();
remoteMediaPlayer.setOnStatusUpdatedListener(this);
remoteMediaPlayer.setOnMetadataUpdatedListener(this);
mSessionId = result.getSessionId();
if (!verifySession(callback)) {
return;
}
try {
Cast.CastApi.setMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
} catch (IOException e) {
debug("Exception while creating media channel", e);
}
startPlayback();
} else {
sendError(callback, status.toString());
}
}
private void startPlayback() {
MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
mediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
MediaInfo mediaInfo = new MediaInfo.Builder(url)
.setContentType(type)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(mediaMetadata)
.build();
try {
remoteMediaPlayer.load(apiClient, mediaInfo, true).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
if (result.getStatus().isSuccess()) {
sendSuccess(callback, null);
debug("Media loaded successfully");
return;
}
debug("Media load failed " + result.getStatus());
sendError(callback, result.getStatus().toString());
}
});
return;
} catch (IllegalStateException e) {
debug("Problem occurred with media during loading", e);
} catch (Exception e) {
debug("Problem opening media during loading", e);
}
sendError(callback, "");
}
}
public ChromeCastPlayer(Context context, RouteInfo route) {
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
if (status != ConnectionResult.SUCCESS) {
throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
}
this.context = context;
this.route = route;
this.canMirror = route.supportsControlCategory(CastMediaControlIntent.categoryForCast(MIRROR_RECEIVER_APP_ID));
}
/**
* This dumps everything we can find about the device into JSON. This will hopefully make it
* easier to filter out duplicate devices from different sources in JS.
* Returns null if the device can't be found.
*/
@Override
public JSONObject toJSON() {
final JSONObject obj = new JSONObject();
try {
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
if (device == null) {
return null;
}
obj.put("uuid", route.getId());
obj.put("version", device.getDeviceVersion());
obj.put("friendlyName", device.getFriendlyName());
obj.put("location", device.getIpAddress().toString());
obj.put("modelName", device.getModelName());
obj.put("mirror", canMirror);
// For now we just assume all of these are Google devices
obj.put("manufacturer", "Google Inc.");
} catch (JSONException ex) {
debug("Error building route", ex);
}
return obj;
}
@Override
public void load(final String title, final String url, final String type, final EventCallback callback) {
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
@Override
public void onApplicationStatusChanged() { }
@Override
public void onVolumeChanged() { }
@Override
public void onApplicationDisconnected(int errorCode) { }
});
apiClient = new GoogleApiClient.Builder(context)
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle connectionHint) {
// Sometimes apiClient is null here. See bug 1061032
if (apiClient != null && !apiClient.isConnected()) {
debug("Connection failed");
sendError(callback, "Not connected");
return;
}
// Launch the media player app and launch this url once its loaded
try {
Cast.CastApi.launchApplication(apiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, true)
.setResultCallback(new VideoPlayCallback(url, type, title, callback));
} catch (Exception e) {
debug("Failed to launch application", e);
}
}
@Override
public void onConnectionSuspended(int cause) {
debug("suspended");
}
}).build();
apiClient.connect();
}
@Override
public void start(final EventCallback callback) {
// Nothing to be done here
sendSuccess(callback, null);
}
@Override
public void stop(final EventCallback callback) {
// Nothing to be done here
sendSuccess(callback, null);
}
public boolean verifySession(final EventCallback callback) {
String msg = null;
if (apiClient == null || !apiClient.isConnected()) {
msg = "Not connected";
}
if (mSessionId == null) {
msg = "No session";
}
if (msg != null) {
debug(msg);
if (callback != null) {
sendError(callback, msg);
}
return false;
}
return true;
}
@Override
public void play(final EventCallback callback) {
if (!verifySession(callback)) {
return;
}
try {
remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (!status.isSuccess()) {
debug("Unable to play: " + status.getStatusCode());
sendError(callback, status.toString());
} else {
sendSuccess(callback, null);
}
}
});
} catch (IllegalStateException ex) {
// The media player may throw if the session has been killed. For now, we're just catching this here.
sendError(callback, "Error playing");
}
}
@Override
public void pause(final EventCallback callback) {
if (!verifySession(callback)) {
return;
}
try {
remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (!status.isSuccess()) {
debug("Unable to pause: " + status.getStatusCode());
sendError(callback, status.toString());
} else {
sendSuccess(callback, null);
}
}
});
} catch (IllegalStateException ex) {
// The media player may throw if the session has been killed. For now, we're just catching this here.
sendError(callback, "Error pausing");
}
}
@Override
public void end(final EventCallback callback) {
if (!verifySession(callback)) {
return;
}
try {
Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
if (result.isSuccess()) {
try {
Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace());
remoteMediaPlayer = null;
mSessionId = null;
apiClient.disconnect();
apiClient = null;
if (callback != null) {
sendSuccess(callback, null);
}
return;
} catch (Exception ex) {
debug("Error ending", ex);
}
}
if (callback != null) {
sendError(callback, result.getStatus().toString());
}
}
});
} catch (IllegalStateException ex) {
// The media player may throw if the session has been killed. For now, we're just catching this here.
sendError(callback, "Error stopping");
}
}
class MirrorChannel implements MessageReceivedCallback {
/**
* @return custom namespace
*/
public String getNamespace() {
return "urn:x-cast:org.mozilla.mirror";
}
/*
* Receive message from the receiver app
*/
@Override
public void onMessageReceived(CastDevice castDevice, String namespace,
String message) {
GeckoAppShell.notifyObservers("MediaPlayer:Response", message);
}
public void sendMessage(String message) {
if (apiClient != null && mMirrorChannel != null) {
try {
Cast.CastApi.sendMessage(apiClient, mMirrorChannel.getNamespace(), message)
.setResultCallback(
new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
}
});
} catch (Exception e) {
Log.e(LOGTAG, "Exception while sending message", e);
}
}
}
}
private class MirrorCallback implements ResultCallback<ApplicationConnectionResult> {
final EventCallback callback;
MirrorCallback(final EventCallback callback) {
this.callback = callback;
}
@Override
public void onResult(ApplicationConnectionResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
mSessionId = result.getSessionId();
String applicationStatus = result.getApplicationStatus();
boolean wasLaunched = result.getWasLaunched();
mApplicationStarted = true;
// Create the custom message
// channel
mMirrorChannel = new MirrorChannel();
try {
Cast.CastApi.setMessageReceivedCallbacks(apiClient,
mMirrorChannel
.getNamespace(),
mMirrorChannel);
sendSuccess(callback, null);
} catch (IOException e) {
Log.e(LOGTAG, "Exception while creating channel", e);
}
GeckoAppShell.notifyObservers("Casting:Mirror", route.getId());
} else {
sendError(callback, status.toString());
}
}
}
@Override
public void message(String msg, final EventCallback callback) {
if (mMirrorChannel != null) {
mMirrorChannel.sendMessage(msg);
}
}
@Override
public void mirror(final EventCallback callback) {
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
@Override
public void onApplicationStatusChanged() { }
@Override
public void onVolumeChanged() { }
@Override
public void onApplicationDisconnected(int errorCode) { }
});
apiClient = new GoogleApiClient.Builder(context)
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle connectionHint) {
// Sometimes apiClient is null here. See bug 1061032
if (apiClient == null || !apiClient.isConnected()) {
return;
}
// Launch the media player app and launch this url once its loaded
try {
Cast.CastApi.launchApplication(apiClient, MIRROR_RECEIVER_APP_ID, true)
.setResultCallback(new MirrorCallback(callback));
} catch (Exception e) {
debug("Failed to launch application", e);
}
}
@Override
public void onConnectionSuspended(int cause) {
debug("suspended");
}
}).build();
apiClient.connect();
}
private static final String LOGTAG = "GeckoChromeCastPlayer";
private void debug(String msg, Exception e) {
if (SHOW_DEBUG) {
Log.e(LOGTAG, msg, e);
}
}
private void debug(String msg) {
if (SHOW_DEBUG) {
Log.d(LOGTAG, msg);
}
}
}

View file

@ -0,0 +1,480 @@
/* -*- Mode: Java; 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;
import java.util.HashMap;
import java.util.Map;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.util.zip.GZIPOutputStream;
import org.mozilla.gecko.AppConstants.Versions;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
@SuppressLint("Registered") // This activity is only registered in the manifest if MOZ_CRASHREPORTER is set
public class CrashReporter extends AppCompatActivity
{
private static final String LOGTAG = "GeckoCrashReporter";
private static final String PASSED_MINI_DUMP_KEY = "minidumpPath";
private static final String PASSED_MINI_DUMP_SUCCESS_KEY = "minidumpSuccess";
private static final String MINI_DUMP_PATH_KEY = "upload_file_minidump";
private static final String PAGE_URL_KEY = "URL";
private static final String NOTES_KEY = "Notes";
private static final String SERVER_URL_KEY = "ServerURL";
private static final String CRASH_REPORT_SUFFIX = "/mozilla/Crash Reports/";
private static final String PENDING_SUFFIX = CRASH_REPORT_SUFFIX + "pending";
private static final String SUBMITTED_SUFFIX = CRASH_REPORT_SUFFIX + "submitted";
private static final String PREFS_SEND_REPORT = "sendReport";
private static final String PREFS_INCLUDE_URL = "includeUrl";
private static final String PREFS_ALLOW_CONTACT = "allowContact";
private static final String PREFS_CONTACT_EMAIL = "contactEmail";
private Handler mHandler;
private ProgressDialog mProgressDialog;
private File mPendingMinidumpFile;
private File mPendingExtrasFile;
private HashMap<String, String> mExtrasStringMap;
private boolean mMinidumpSucceeded;
private boolean moveFile(File inFile, File outFile) {
Log.i(LOGTAG, "moving " + inFile + " to " + outFile);
if (inFile.renameTo(outFile))
return true;
try {
outFile.createNewFile();
Log.i(LOGTAG, "couldn't rename minidump file");
// so copy it instead
FileChannel inChannel = new FileInputStream(inFile).getChannel();
FileChannel outChannel = new FileOutputStream(outFile).getChannel();
long transferred = inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
outChannel.close();
if (transferred > 0)
inFile.delete();
} catch (Exception e) {
Log.e(LOGTAG, "exception while copying minidump file: ", e);
return false;
}
return true;
}
private void doFinish() {
if (mHandler != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
finish();
}
});
}
}
@Override
public void finish() {
try {
if (mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
} catch (Exception e) {
Log.e(LOGTAG, "exception while closing progress dialog: ", e);
}
super.finish();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// mHandler is created here so runnables can be run on the main thread
mHandler = new Handler();
setContentView(R.layout.crash_reporter);
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setMessage(getString(R.string.sending_crash_report));
mMinidumpSucceeded = getIntent().getBooleanExtra(PASSED_MINI_DUMP_SUCCESS_KEY, false);
if (!mMinidumpSucceeded) {
Log.i(LOGTAG, "Failed to get minidump.");
}
String passedMinidumpPath = getIntent().getStringExtra(PASSED_MINI_DUMP_KEY);
File passedMinidumpFile = new File(passedMinidumpPath);
File pendingDir = new File(getFilesDir(), PENDING_SUFFIX);
pendingDir.mkdirs();
mPendingMinidumpFile = new File(pendingDir, passedMinidumpFile.getName());
moveFile(passedMinidumpFile, mPendingMinidumpFile);
File extrasFile = new File(passedMinidumpPath.replaceAll("\\.dmp", ".extra"));
mPendingExtrasFile = new File(pendingDir, extrasFile.getName());
moveFile(extrasFile, mPendingExtrasFile);
mExtrasStringMap = new HashMap<String, String>();
readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap);
// Notify GeckoApp that we've crashed, so it can react appropriately during the next start.
try {
File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED");
crashFlag.createNewFile();
} catch (GeckoProfileDirectories.NoMozillaDirectoryException | IOException e) {
Log.e(LOGTAG, "Cannot set crash flag: ", e);
}
final CheckBox allowContactCheckBox = (CheckBox) findViewById(R.id.allow_contact);
final CheckBox includeUrlCheckBox = (CheckBox) findViewById(R.id.include_url);
final CheckBox sendReportCheckBox = (CheckBox) findViewById(R.id.send_report);
final EditText commentsEditText = (EditText) findViewById(R.id.comment);
final EditText emailEditText = (EditText) findViewById(R.id.email);
// Load CrashReporter preferences to avoid redundant user input.
SharedPreferences prefs = GeckoSharedPrefs.forCrashReporter(this);
final boolean sendReport = prefs.getBoolean(PREFS_SEND_REPORT, true);
final boolean includeUrl = prefs.getBoolean(PREFS_INCLUDE_URL, false);
final boolean allowContact = prefs.getBoolean(PREFS_ALLOW_CONTACT, false);
final String contactEmail = prefs.getString(PREFS_CONTACT_EMAIL, "");
allowContactCheckBox.setChecked(allowContact);
includeUrlCheckBox.setChecked(includeUrl);
sendReportCheckBox.setChecked(sendReport);
emailEditText.setText(contactEmail);
sendReportCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
commentsEditText.setEnabled(isChecked);
commentsEditText.requestFocus();
includeUrlCheckBox.setEnabled(isChecked);
allowContactCheckBox.setEnabled(isChecked);
emailEditText.setEnabled(isChecked && allowContactCheckBox.isChecked());
}
});
allowContactCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
// We need to check isEnabled() here because this listener is
// fired on rotation -- even when the checkbox is disabled.
emailEditText.setEnabled(checkbox.isEnabled() && isChecked);
emailEditText.requestFocus();
}
});
emailEditText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Even if the email EditText is disabled, allow it to be
// clicked and focused.
if (sendReportCheckBox.isChecked() && !v.isEnabled()) {
allowContactCheckBox.setChecked(true);
v.setEnabled(true);
v.requestFocus();
}
}
});
}
@Override
public void onBackPressed() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.crash_closing_alert);
builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
CrashReporter.this.finish();
}
});
builder.show();
}
private void backgroundSendReport() {
final CheckBox sendReportCheckbox = (CheckBox) findViewById(R.id.send_report);
if (!sendReportCheckbox.isChecked()) {
doFinish();
return;
}
// Persist settings to avoid redundant user input.
savePrefs();
mProgressDialog.show();
new Thread(new Runnable() {
@Override
public void run() {
sendReport(mPendingMinidumpFile, mExtrasStringMap, mPendingExtrasFile);
}
}, "CrashReporter Thread").start();
}
private void savePrefs() {
SharedPreferences.Editor editor = GeckoSharedPrefs.forCrashReporter(this).edit();
final boolean allowContact = ((CheckBox) findViewById(R.id.allow_contact)).isChecked();
final boolean includeUrl = ((CheckBox) findViewById(R.id.include_url)).isChecked();
final boolean sendReport = ((CheckBox) findViewById(R.id.send_report)).isChecked();
final String contactEmail = ((EditText) findViewById(R.id.email)).getText().toString();
editor.putBoolean(PREFS_ALLOW_CONTACT, allowContact);
editor.putBoolean(PREFS_INCLUDE_URL, includeUrl);
editor.putBoolean(PREFS_SEND_REPORT, sendReport);
editor.putString(PREFS_CONTACT_EMAIL, contactEmail);
// A slight performance improvement via async apply() vs. blocking on commit().
editor.apply();
}
public void onCloseClick(View v) { // bound via crash_reporter.xml
backgroundSendReport();
}
public void onRestartClick(View v) { // bound via crash_reporter.xml
doRestart();
backgroundSendReport();
}
private boolean readStringsFromFile(String filePath, Map<String, String> stringMap) {
try {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
return readStringsFromReader(reader, stringMap);
} catch (Exception e) {
Log.e(LOGTAG, "exception while reading strings: ", e);
return false;
}
}
private boolean readStringsFromReader(BufferedReader reader, Map<String, String> stringMap) throws IOException {
String line;
while ((line = reader.readLine()) != null) {
int equalsPos = -1;
if ((equalsPos = line.indexOf('=')) != -1) {
String key = line.substring(0, equalsPos);
String val = unescape(line.substring(equalsPos + 1));
stringMap.put(key, val);
}
}
reader.close();
return true;
}
private String generateBoundary() {
// Generate some random numbers to fill out the boundary
int r0 = (int)(Integer.MAX_VALUE * Math.random());
int r1 = (int)(Integer.MAX_VALUE * Math.random());
return String.format("---------------------------%08X%08X", r0, r1);
}
private void sendPart(OutputStream os, String boundary, String name, String data) {
try {
os.write(("--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"" + name + "\"\r\n" +
"\r\n" +
data + "\r\n"
).getBytes());
} catch (Exception ex) {
Log.e(LOGTAG, "Exception when sending \"" + name + "\"", ex);
}
}
private void sendFile(OutputStream os, String boundary, String name, File file) throws IOException {
os.write(("--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"" + name + "\"; " +
"filename=\"" + file.getName() + "\"\r\n" +
"Content-Type: application/octet-stream\r\n" +
"\r\n"
).getBytes());
FileChannel fc = new FileInputStream(file).getChannel();
fc.transferTo(0, fc.size(), Channels.newChannel(os));
fc.close();
}
private String readLogcat() {
final String crashReporterProc = " " + android.os.Process.myPid() + ' ';
BufferedReader br = null;
try {
// get at most the last 400 lines of logcat
Process proc = Runtime.getRuntime().exec(new String[] {
"logcat", "-v", "threadtime", "-t", "400", "-d", "*:D"
});
StringBuilder sb = new StringBuilder();
br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
for (String s = br.readLine(); s != null; s = br.readLine()) {
if (s.contains(crashReporterProc)) {
// Don't include logs from the crash reporter's process.
break;
}
sb.append(s).append('\n');
}
return sb.toString();
} catch (Exception e) {
return "Unable to get logcat: " + e.toString();
} finally {
if (br != null) {
try {
br.close();
} catch (Exception e) {
// ignore
}
}
}
}
private void sendReport(File minidumpFile, Map<String, String> extras, File extrasFile) {
Log.i(LOGTAG, "sendReport: " + minidumpFile.getPath());
final CheckBox includeURLCheckbox = (CheckBox) findViewById(R.id.include_url);
String spec = extras.get(SERVER_URL_KEY);
if (spec == null) {
doFinish();
return;
}
Log.i(LOGTAG, "server url: " + spec);
try {
URL url = new URL(spec);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("POST");
String boundary = generateBoundary();
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
conn.setRequestProperty("Content-Encoding", "gzip");
OutputStream os = new GZIPOutputStream(conn.getOutputStream());
for (String key : extras.keySet()) {
if (key.equals(PAGE_URL_KEY)) {
if (includeURLCheckbox.isChecked())
sendPart(os, boundary, key, extras.get(key));
} else if (!key.equals(SERVER_URL_KEY) && !key.equals(NOTES_KEY)) {
sendPart(os, boundary, key, extras.get(key));
}
}
// Add some extra information to notes so its displayed by
// crash-stats.mozilla.org. Remove this when bug 607942 is fixed.
StringBuilder sb = new StringBuilder();
sb.append(extras.containsKey(NOTES_KEY) ? extras.get(NOTES_KEY) + "\n" : "");
if (AppConstants.MOZ_MIN_CPU_VERSION < 7) {
sb.append("nothumb Build\n");
}
sb.append(Build.MANUFACTURER).append(' ')
.append(Build.MODEL).append('\n')
.append(Build.FINGERPRINT);
sendPart(os, boundary, NOTES_KEY, sb.toString());
sendPart(os, boundary, "Min_ARM_Version", Integer.toString(AppConstants.MOZ_MIN_CPU_VERSION));
sendPart(os, boundary, "Android_Manufacturer", Build.MANUFACTURER);
sendPart(os, boundary, "Android_Model", Build.MODEL);
sendPart(os, boundary, "Android_Board", Build.BOARD);
sendPart(os, boundary, "Android_Brand", Build.BRAND);
sendPart(os, boundary, "Android_Device", Build.DEVICE);
sendPart(os, boundary, "Android_Display", Build.DISPLAY);
sendPart(os, boundary, "Android_Fingerprint", Build.FINGERPRINT);
sendPart(os, boundary, "Android_APP_ABI", AppConstants.MOZ_APP_ABI);
sendPart(os, boundary, "Android_CPU_ABI", Build.CPU_ABI);
sendPart(os, boundary, "Android_MIN_SDK", Integer.toString(AppConstants.Versions.MIN_SDK_VERSION));
sendPart(os, boundary, "Android_MAX_SDK", Integer.toString(AppConstants.Versions.MAX_SDK_VERSION));
try {
sendPart(os, boundary, "Android_CPU_ABI2", Build.CPU_ABI2);
sendPart(os, boundary, "Android_Hardware", Build.HARDWARE);
} catch (Exception ex) {
Log.e(LOGTAG, "Exception while sending SDK version 8 keys", ex);
}
sendPart(os, boundary, "Android_Version", Build.VERSION.SDK_INT + " (" + Build.VERSION.CODENAME + ")");
if (Versions.feature16Plus && includeURLCheckbox.isChecked()) {
sendPart(os, boundary, "Android_Logcat", readLogcat());
}
String comment = ((EditText) findViewById(R.id.comment)).getText().toString();
if (!TextUtils.isEmpty(comment)) {
sendPart(os, boundary, "Comments", comment);
}
if (((CheckBox) findViewById(R.id.allow_contact)).isChecked()) {
String email = ((EditText) findViewById(R.id.email)).getText().toString();
sendPart(os, boundary, "Email", email);
}
sendPart(os, boundary, PASSED_MINI_DUMP_SUCCESS_KEY, mMinidumpSucceeded ? "True" : "False");
sendFile(os, boundary, MINI_DUMP_PATH_KEY, minidumpFile);
os.write(("\r\n--" + boundary + "--\r\n").getBytes());
os.flush();
os.close();
BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
HashMap<String, String> responseMap = new HashMap<String, String>();
readStringsFromReader(br, responseMap);
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
File submittedDir = new File(getFilesDir(),
SUBMITTED_SUFFIX);
submittedDir.mkdirs();
minidumpFile.delete();
extrasFile.delete();
String crashid = responseMap.get("CrashID");
File file = new File(submittedDir, crashid + ".txt");
FileOutputStream fos = new FileOutputStream(file);
fos.write("Crash ID: ".getBytes());
fos.write(crashid.getBytes());
fos.close();
} else {
Log.i(LOGTAG, "Received failure HTTP response code from server: " + conn.getResponseCode());
}
} catch (IOException e) {
Log.e(LOGTAG, "exception during send: ", e);
}
doFinish();
}
private void doRestart() {
try {
String action = "android.intent.action.MAIN";
Intent intent = new Intent(action);
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
intent.putExtra("didRestart", true);
Log.i(LOGTAG, intent.toString());
startActivity(intent);
} catch (Exception e) {
Log.e(LOGTAG, "error while trying to restart", e);
}
}
private String unescape(String string) {
return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t");
}
}

View file

@ -0,0 +1,89 @@
/* -*- 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;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.widget.themed.ThemedEditText;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
public class CustomEditText extends ThemedEditText {
private OnKeyPreImeListener mOnKeyPreImeListener;
private OnSelectionChangedListener mOnSelectionChangedListener;
private OnWindowFocusChangeListener mOnWindowFocusChangeListener;
private int mHighlightColor;
public CustomEditText(Context context, AttributeSet attrs) {
super(context, attrs);
setPrivateMode(false); // Initialize mHighlightColor.
}
public interface OnKeyPreImeListener {
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event);
}
public void setOnKeyPreImeListener(OnKeyPreImeListener listener) {
mOnKeyPreImeListener = listener;
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (mOnKeyPreImeListener != null)
return mOnKeyPreImeListener.onKeyPreIme(this, keyCode, event);
return false;
}
public interface OnSelectionChangedListener {
public void onSelectionChanged(int selStart, int selEnd);
}
public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
mOnSelectionChangedListener = listener;
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
if (mOnSelectionChangedListener != null)
mOnSelectionChangedListener.onSelectionChanged(selStart, selEnd);
super.onSelectionChanged(selStart, selEnd);
}
public interface OnWindowFocusChangeListener {
public void onWindowFocusChanged(boolean hasFocus);
}
public void setOnWindowFocusChangeListener(OnWindowFocusChangeListener listener) {
mOnWindowFocusChangeListener = listener;
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (mOnWindowFocusChangeListener != null)
mOnWindowFocusChangeListener.onWindowFocusChanged(hasFocus);
}
// Provide a getHighlightColor implementation for API level < 16.
@Override
public int getHighlightColor() {
return mHighlightColor;
}
@Override
public void setPrivateMode(boolean isPrivate) {
super.setPrivateMode(isPrivate);
mHighlightColor = ContextCompat.getColor(getContext(), isPrivate
? R.color.url_bar_text_highlight_pb : R.color.fennec_ui_orange);
// android:textColorHighlight cannot support a ColorStateList.
setHighlightColor(mHighlightColor);
}
}

View file

@ -0,0 +1,133 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.preferences.GeckoPreferences;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.support.v4.app.NotificationCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.StyleSpan;
public class DataReportingNotification {
private static final String LOGTAG = "DataReportNotification";
public static final String ALERT_NAME_DATAREPORTING_NOTIFICATION = "datareporting-notification";
private static final String PREFS_POLICY_NOTIFIED_TIME = "datareporting.policy.dataSubmissionPolicyNotifiedTime";
private static final String PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion";
private static final int DATA_REPORTING_VERSION = 2;
public static void checkAndNotifyPolicy(Context context) {
SharedPreferences dataPrefs = GeckoSharedPrefs.forApp(context);
final int currentVersion = dataPrefs.getInt(PREFS_POLICY_VERSION, -1);
if (currentVersion < 1) {
// This is a first run, so notify user about data policy.
notifyDataPolicy(context, dataPrefs);
// If healthreport is enabled, set default preference value.
if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
SharedPreferences.Editor editor = dataPrefs.edit();
editor.putBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
editor.apply();
}
return;
}
if (currentVersion == 1) {
// Redisplay notification only for Beta because version 2 updates Beta policy and update version.
if (TextUtils.equals("beta", AppConstants.MOZ_UPDATE_CHANNEL)) {
notifyDataPolicy(context, dataPrefs);
} else {
// Silently update the version.
SharedPreferences.Editor editor = dataPrefs.edit();
editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
editor.apply();
}
return;
}
if (currentVersion >= DATA_REPORTING_VERSION) {
// Do nothing, we're at a current (or future) version.
return;
}
}
/**
* Launch a notification of the data policy, and record notification time and version.
*/
public static void notifyDataPolicy(Context context, SharedPreferences sharedPrefs) {
boolean result = false;
try {
// Launch main App to launch Data choices when notification is clicked.
Intent prefIntent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
prefIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
GeckoPreferences.setResourceToOpen(prefIntent, "preferences_privacy");
prefIntent.putExtra(ALERT_NAME_DATAREPORTING_NOTIFICATION, true);
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT);
final Resources resources = context.getResources();
// Create and send notification.
String notificationTitle = resources.getString(R.string.datareporting_notification_title);
String notificationSummary;
if (Versions.preJB) {
notificationSummary = resources.getString(R.string.datareporting_notification_action);
} else {
// Display partial version of Big Style notification for supporting devices.
notificationSummary = resources.getString(R.string.datareporting_notification_summary);
}
String notificationAction = resources.getString(R.string.datareporting_notification_action);
String notificationBigSummary = resources.getString(R.string.datareporting_notification_summary);
// Make styled ticker text for display in notification bar.
String tickerString = resources.getString(R.string.datareporting_notification_ticker_text);
SpannableString tickerText = new SpannableString(tickerString);
// Bold the notification title of the ticker text, which is the same string as notificationTitle.
tickerText.setSpan(new StyleSpan(Typeface.BOLD), 0, notificationTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
Notification notification = new NotificationCompat.Builder(context)
.setContentTitle(notificationTitle)
.setContentText(notificationSummary)
.setSmallIcon(R.drawable.ic_status_logo)
.setAutoCancel(true)
.setContentIntent(contentIntent)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(notificationBigSummary))
.addAction(R.drawable.firefox_settings_alert, notificationAction, contentIntent)
.setTicker(tickerText)
.build();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
int notificationID = ALERT_NAME_DATAREPORTING_NOTIFICATION.hashCode();
notificationManager.notify(notificationID, notification);
// Record version and notification time.
SharedPreferences.Editor editor = sharedPrefs.edit();
long now = System.currentTimeMillis();
editor.putLong(PREFS_POLICY_NOTIFIED_TIME, now);
editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
editor.apply();
result = true;
} finally {
// We want to track any errors, so record notification outcome.
Telemetry.sendUIEvent(TelemetryContract.Event.POLICY_NOTIFICATION_SUCCESS, result);
}
}
}

View file

@ -0,0 +1,52 @@
/* -*- 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;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.util.Log;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.InputOptionsUtils;
/**
* Supports the DevTools WiFi debugging authentication flow by invoking a QR decoder.
*/
public class DevToolsAuthHelper {
private static final String LOGTAG = "GeckoDevToolsAuthHelper";
public static void scan(Context context, final EventCallback callback) {
final Intent intent = InputOptionsUtils.createQRCodeReaderIntent();
intent.putExtra("PROMPT_MESSAGE", context.getString(R.string.devtools_auth_scan_header));
// Check ahead of time if an activity exists for the intent. This
// avoids a case where we get both an ActivityNotFoundException *and*
// an activity result when the activity is missing.
PackageManager pm = context.getPackageManager();
if (pm.resolveActivity(intent, 0) == null) {
Log.w(LOGTAG, "PackageManager can't resolve the activity.");
callback.sendError("PackageManager can't resolve the activity.");
return;
}
ActivityHandlerHelper.startIntent(intent, new ActivityResultHandler() {
@Override
public void onActivityResult(int resultCode, Intent intent) {
if (resultCode == Activity.RESULT_OK) {
String text = intent.getStringExtra("SCAN_RESULT");
callback.sendSuccess(text);
} else {
callback.sendError(resultCode);
}
}
});
}
}

View file

@ -0,0 +1,361 @@
/* -*- 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;
import java.util.HashSet;
import android.text.TextUtils;
import android.widget.PopupWindow;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.AnchoredPopup;
import org.mozilla.gecko.widget.DoorHanger;
import android.content.Context;
import android.util.Log;
import android.view.View;
import org.mozilla.gecko.widget.DoorhangerConfig;
public class DoorHangerPopup extends AnchoredPopup
implements GeckoEventListener,
Tabs.OnTabsChangedListener,
PopupWindow.OnDismissListener,
DoorHanger.OnButtonClickListener {
private static final String LOGTAG = "GeckoDoorHangerPopup";
// Stores a set of all active DoorHanger notifications. A DoorHanger is
// uniquely identified by its tabId and value.
private final HashSet<DoorHanger> mDoorHangers;
// Whether or not the doorhanger popup is disabled.
private boolean mDisabled;
public DoorHangerPopup(Context context) {
super(context);
mDoorHangers = new HashSet<DoorHanger>();
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"Doorhanger:Add",
"Doorhanger:Remove");
Tabs.registerOnTabsChangedListener(this);
setOnDismissListener(this);
}
void destroy() {
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"Doorhanger:Add",
"Doorhanger:Remove");
Tabs.unregisterOnTabsChangedListener(this);
}
/**
* Temporarily disables the doorhanger popup. If the popup is disabled,
* it will not be shown to the user, but it will continue to process
* calls to add/remove doorhanger notifications.
*/
void disable() {
mDisabled = true;
updatePopup();
}
/**
* Re-enables the doorhanger popup.
*/
void enable() {
mDisabled = false;
updatePopup();
}
@Override
public void handleMessage(String event, JSONObject geckoObject) {
try {
if (event.equals("Doorhanger:Add")) {
final DoorhangerConfig config = makeConfigFromJSON(geckoObject);
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
addDoorHanger(config);
}
});
} else if (event.equals("Doorhanger:Remove")) {
final int tabId = geckoObject.getInt("tabID");
final String value = geckoObject.getString("value");
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
DoorHanger doorHanger = getDoorHanger(tabId, value);
if (doorHanger == null)
return;
removeDoorHanger(doorHanger);
updatePopup();
}
});
}
} catch (Exception e) {
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
}
}
private DoorhangerConfig makeConfigFromJSON(JSONObject json) throws JSONException {
final int tabId = json.getInt("tabID");
final String id = json.getString("value");
final String typeString = json.optString("category");
DoorHanger.Type doorhangerType = DoorHanger.Type.DEFAULT;
if (DoorHanger.Type.LOGIN.toString().equals(typeString)) {
doorhangerType = DoorHanger.Type.LOGIN;
} else if (DoorHanger.Type.GEOLOCATION.toString().equals(typeString)) {
doorhangerType = DoorHanger.Type.GEOLOCATION;
} else if (DoorHanger.Type.DESKTOPNOTIFICATION2.toString().equals(typeString)) {
doorhangerType = DoorHanger.Type.DESKTOPNOTIFICATION2;
} else if (DoorHanger.Type.WEBRTC.toString().equals(typeString)) {
doorhangerType = DoorHanger.Type.WEBRTC;
} else if (DoorHanger.Type.VIBRATION.toString().equals(typeString)) {
doorhangerType = DoorHanger.Type.VIBRATION;
}
final DoorhangerConfig config = new DoorhangerConfig(tabId, id, doorhangerType, this);
config.setMessage(json.getString("message"));
config.setOptions(json.getJSONObject("options"));
final JSONArray buttonArray = json.getJSONArray("buttons");
int numButtons = buttonArray.length();
if (numButtons > 2) {
Log.e(LOGTAG, "Doorhanger can have a maximum of two buttons!");
numButtons = 2;
}
for (int i = 0; i < numButtons; i++) {
final JSONObject buttonJSON = buttonArray.getJSONObject(i);
final boolean isPositive = buttonJSON.optBoolean("positive", false);
config.setButton(buttonJSON.getString("label"), buttonJSON.getInt("callback"), isPositive);
}
return config;
}
// This callback is automatically executed on the UI thread.
@Override
public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) {
switch (msg) {
case CLOSED:
// Remove any doorhangers for a tab when it's closed (make
// a temporary set to avoid a ConcurrentModificationException)
removeTabDoorHangers(tab.getId(), true);
break;
case LOCATION_CHANGE:
// Only remove doorhangers if the popup is hidden or if we're navigating to a new URL
if (!isShowing() || !data.equals(tab.getURL()))
removeTabDoorHangers(tab.getId(), false);
// Update the popup if the location change was on the current tab
if (Tabs.getInstance().isSelectedTab(tab))
updatePopup();
break;
case SELECTED:
// Always update the popup when a new tab is selected. This will cover cases
// where a different tab was closed, since we always need to select a new tab.
updatePopup();
break;
}
}
/**
* Adds a doorhanger.
*
* This method must be called on the UI thread.
*/
void addDoorHanger(DoorhangerConfig config) {
final int tabId = config.getTabId();
// Don't add a doorhanger for a tab that doesn't exist
if (Tabs.getInstance().getTab(tabId) == null) {
return;
}
// Replace the doorhanger if it already exists
DoorHanger oldDoorHanger = getDoorHanger(tabId, config.getId());
if (oldDoorHanger != null) {
removeDoorHanger(oldDoorHanger);
}
if (!mInflated) {
init();
}
final DoorHanger newDoorHanger = DoorHanger.Get(mContext, config);
mDoorHangers.add(newDoorHanger);
mContent.addView(newDoorHanger);
// Only update the popup if we're adding a notification to the selected tab
if (tabId == Tabs.getInstance().getSelectedTab().getId())
updatePopup();
}
/*
* DoorHanger.OnButtonClickListener implementation
*/
@Override
public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
GeckoAppShell.notifyObservers("Doorhanger:Reply", response.toString());
removeDoorHanger(doorhanger);
updatePopup();
}
/**
* Gets a doorhanger.
*
* This method must be called on the UI thread.
*/
DoorHanger getDoorHanger(int tabId, String value) {
for (DoorHanger dh : mDoorHangers) {
if (dh.getTabId() == tabId && dh.getIdentifier().equals(value))
return dh;
}
// If there's no doorhanger for the given tabId and value, return null
return null;
}
/**
* Removes a doorhanger.
*
* This method must be called on the UI thread.
*/
void removeDoorHanger(final DoorHanger doorHanger) {
mDoorHangers.remove(doorHanger);
mContent.removeView(doorHanger);
}
/**
* Removes doorhangers for a given tab.
* @param tabId identifier of the tab to remove doorhangers from
* @param forceRemove boolean for force-removing tabs. If true, all doorhangers associated
* with the tab specified are removed; if false, only remove the doorhangers
* that are not persistent, as specified by the doorhanger options.
*
* This method must be called on the UI thread.
*/
void removeTabDoorHangers(int tabId, boolean forceRemove) {
// Make a temporary set to avoid a ConcurrentModificationException
HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>();
for (DoorHanger dh : mDoorHangers) {
// Only remove transient doorhangers for the given tab
if (dh.getTabId() == tabId
&& (forceRemove || (!forceRemove && dh.shouldRemove(isShowing())))) {
doorHangersToRemove.add(dh);
}
}
for (DoorHanger dh : doorHangersToRemove) {
removeDoorHanger(dh);
}
}
/**
* Updates the popup state.
*
* This method must be called on the UI thread.
*/
void updatePopup() {
// Bail if the selected tab is null, if there are no active doorhangers,
// if we haven't inflated the layout yet (this can happen if updatePopup()
// is called before the runnable from addDoorHanger() runs), or if the
// doorhanger popup is temporarily disabled.
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab == null || mDoorHangers.size() == 0 || !mInflated || mDisabled) {
dismiss();
return;
}
// Show doorhangers for the selected tab
int tabId = tab.getId();
boolean shouldShowPopup = false;
DoorHanger firstDoorhanger = null;
for (DoorHanger dh : mDoorHangers) {
if (dh.getTabId() == tabId) {
dh.setVisibility(View.VISIBLE);
shouldShowPopup = true;
if (firstDoorhanger == null) {
firstDoorhanger = dh;
} else {
dh.hideTitle();
}
} else {
dh.setVisibility(View.GONE);
}
}
// Dismiss the popup if there are no doorhangers to show for this tab
if (!shouldShowPopup) {
dismiss();
return;
}
showDividers();
final String baseDomain = tab.getBaseDomain();
if (TextUtils.isEmpty(baseDomain)) {
firstDoorhanger.hideTitle();
} else {
firstDoorhanger.showTitle(tab.getFavicon(), baseDomain);
}
if (isShowing()) {
show();
return;
}
setFocusable(true);
show();
}
//Show all inter-DoorHanger dividers (ie. Dividers on all visible DoorHangers except the last one)
private void showDividers() {
int count = mContent.getChildCount();
DoorHanger lastVisibleDoorHanger = null;
for (int i = 0; i < count; i++) {
DoorHanger dh = (DoorHanger) mContent.getChildAt(i);
dh.showDivider();
if (dh.getVisibility() == View.VISIBLE) {
lastVisibleDoorHanger = dh;
}
}
if (lastVisibleDoorHanger != null) {
lastVisibleDoorHanger.hideDivider();
}
}
@Override
public void onDismiss() {
final int tabId = Tabs.getInstance().getSelectedTab().getId();
removeTabDoorHangers(tabId, true);
}
@Override
public void dismiss() {
// If the popup is focusable while it is hidden, we run into crashes
// on pre-ICS devices when the popup gets focus before it is shown.
setFocusable(false);
super.dismiss();
}
}

View file

@ -0,0 +1,235 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.permissions.Permissions;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.EventCallback;
import java.io.File;
import java.lang.IllegalArgumentException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import android.app.DownloadManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
public class DownloadsIntegration implements NativeEventListener
{
private static final String LOGTAG = "GeckoDownloadsIntegration";
private static final List<String> UNKNOWN_MIME_TYPES;
static {
final ArrayList<String> tempTypes = new ArrayList<>(3);
tempTypes.add("unknown/unknown"); // This will be used as a default mime type for unknown files
tempTypes.add("application/unknown");
tempTypes.add("application/octet-stream"); // Github uses this for APK files
UNKNOWN_MIME_TYPES = Collections.unmodifiableList(tempTypes);
}
private static final String DOWNLOAD_REMOVE = "Download:Remove";
private DownloadsIntegration() {
EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this, DOWNLOAD_REMOVE);
}
private static DownloadsIntegration sInstance;
private static class Download {
final File file;
final long id;
final private static int UNKNOWN_ID = -1;
public Download(final String path) {
this(path, UNKNOWN_ID);
}
public Download(final String path, final long id) {
file = new File(path);
this.id = id;
}
public static Download fromJSON(final NativeJSObject obj) {
final String path = obj.getString("path");
return new Download(path);
}
public static Download fromCursor(final Cursor c) {
final String path = c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
final long id = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
return new Download(path, id);
}
public boolean equals(final Download download) {
return file.equals(download.file);
}
}
public static void init() {
if (sInstance == null) {
sInstance = new DownloadsIntegration();
}
}
@Override
public void handleMessage(final String event, final NativeJSObject message,
final EventCallback callback) {
if (DOWNLOAD_REMOVE.equals(event)) {
final Download d = Download.fromJSON(message);
removeDownload(d);
}
}
private static boolean useSystemDownloadManager() {
if (!AppConstants.ANDROID_DOWNLOADS_INTEGRATION) {
return false;
}
int state = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
try {
state = GeckoAppShell.getContext().getPackageManager().getApplicationEnabledSetting("com.android.providers.downloads");
} catch (IllegalArgumentException e) {
// Download Manager package does not exist
return false;
}
return (PackageManager.COMPONENT_ENABLED_STATE_ENABLED == state ||
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT == state);
}
@WrapForJNI(calledFrom = "gecko")
public static String getTemporaryDownloadDirectory() {
Context context = GeckoAppShell.getApplicationContext();
if (Permissions.has(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// We do have the STORAGE permission, so we can save the file directly to the public
// downloads directory.
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.getAbsolutePath();
} else {
// Without the permission we are going to start to download the file to the cache
// directory. Later in the process we will ask for the permission and the download
// process will move the file to the actual downloads directory. If we do not get the
// permission then the download will be cancelled.
return context.getCacheDir().getAbsolutePath();
}
}
@WrapForJNI(calledFrom = "gecko")
public static void scanMedia(final String aFile, String aMimeType) {
String mimeType = aMimeType;
if (UNKNOWN_MIME_TYPES.contains(mimeType)) {
// If this is a generic undefined mimetype, erase it so that we can try to determine
// one from the file extension below.
mimeType = "";
}
// If the platform didn't give us a mimetype, try to guess one from the filename
if (TextUtils.isEmpty(mimeType)) {
final int extPosition = aFile.lastIndexOf(".");
if (extPosition > 0 && extPosition < aFile.length() - 1) {
mimeType = GeckoAppShell.getMimeTypeFromExtension(aFile.substring(extPosition + 1));
}
}
// addCompletedDownload will throw if it received any null parameters. Use aMimeType or a default
// if we still don't have one.
if (TextUtils.isEmpty(mimeType)) {
if (TextUtils.isEmpty(aMimeType)) {
mimeType = UNKNOWN_MIME_TYPES.get(0);
} else {
mimeType = aMimeType;
}
}
if (useSystemDownloadManager()) {
final File f = new File(aFile);
final DownloadManager dm = (DownloadManager) GeckoAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
dm.addCompletedDownload(f.getName(),
f.getName(),
true, // Media scanner should scan this
mimeType,
f.getAbsolutePath(),
Math.max(1, f.length()), // Some versions of Android require downloads to be at least length 1
false); // Don't show a notification.
} else {
final Context context = GeckoAppShell.getContext();
final GeckoMediaScannerClient client = new GeckoMediaScannerClient(context, aFile, mimeType);
client.connect();
}
}
public static void removeDownload(final Download download) {
if (!useSystemDownloadManager()) {
return;
}
final DownloadManager dm = (DownloadManager) GeckoAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = null;
try {
c = dm.query((new DownloadManager.Query()).setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
if (c == null || !c.moveToFirst()) {
return;
}
do {
final Download d = Download.fromCursor(c);
// Try hard as we can to verify this download is the one we think it is
if (download.equals(d)) {
dm.remove(d.id);
}
} while (c.moveToNext());
} finally {
if (c != null) {
c.close();
}
}
}
private static final class GeckoMediaScannerClient implements MediaScannerConnectionClient {
private final String mFile;
private final String mMimeType;
private MediaScannerConnection mScanner;
public GeckoMediaScannerClient(Context context, String file, String mimeType) {
mFile = file;
mMimeType = mimeType;
mScanner = new MediaScannerConnection(context, this);
}
public void connect() {
mScanner.connect();
}
@Override
public void onMediaScannerConnected() {
mScanner.scanFile(mFile, mMimeType);
}
@Override
public void onScanCompleted(String path, Uri uri) {
if (path.equals(mFile)) {
mScanner.disconnect();
mScanner = null;
}
}
}
}

View file

@ -0,0 +1,218 @@
package org.mozilla.gecko;
import org.mozilla.gecko.PrefsHelper.PrefHandlerBase;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
import org.mozilla.gecko.gfx.LayerView;
import org.mozilla.gecko.util.ThreadUtils;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
public class DynamicToolbar {
private static final String LOGTAG = "DynamicToolbar";
private static final String STATE_ENABLED = "dynamic_toolbar";
private static final String CHROME_PREF = "browser.chrome.dynamictoolbar";
// DynamicToolbar is enabled iff prefEnabled is true *and* accessibilityEnabled is false,
// so it is disabled by default on startup. We do not enable it until we explicitly get
// the pref from Gecko telling us to turn it on.
private volatile boolean prefEnabled;
private boolean accessibilityEnabled;
// On some device we have to force-disable the dynamic toolbar because of
// bugs in the Android code. See bug 1231554.
private final boolean forceDisabled;
private final PrefsHelper.PrefHandler prefObserver;
private LayerView layerView;
private OnEnabledChangedListener enabledChangedListener;
private boolean temporarilyVisible;
public enum VisibilityTransition {
IMMEDIATE,
ANIMATE
}
/**
* Listener for changes to the dynamic toolbar's enabled state.
*/
public interface OnEnabledChangedListener {
/**
* This callback is executed on the UI thread.
*/
public void onEnabledChanged(boolean enabled);
}
public DynamicToolbar() {
// Listen to the dynamic toolbar pref
prefObserver = new PrefHandler();
PrefsHelper.addObserver(new String[] { CHROME_PREF }, prefObserver);
forceDisabled = isForceDisabled();
if (forceDisabled) {
Log.i(LOGTAG, "Force-disabling dynamic toolbar for " + Build.MODEL + " (" + Build.DEVICE + "/" + Build.PRODUCT + ")");
}
}
public static boolean isForceDisabled() {
// Force-disable dynamic toolbar on the variants of the Galaxy Note 10.1
// and Note 8.0 running Android 4.1.2. (Bug 1231554). This includes
// the following model numbers:
// GT-N8000, GT-N8005, GT-N8010, GT-N8013, GT-N8020
// GT-N5100, GT-N5110, GT-N5120
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN
&& (Build.MODEL.startsWith("GT-N80") ||
Build.MODEL.startsWith("GT-N51"))) {
return true;
}
// Also disable variants of the Galaxy Note 4 on Android 5.0.1 (Bug 1301593)
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
&& (Build.MODEL.startsWith("SM-N910"))) {
return true;
}
return false;
}
public void destroy() {
PrefsHelper.removeObserver(prefObserver);
}
public void setLayerView(LayerView layerView) {
ThreadUtils.assertOnUiThread();
this.layerView = layerView;
}
public void setEnabledChangedListener(OnEnabledChangedListener listener) {
ThreadUtils.assertOnUiThread();
enabledChangedListener = listener;
}
public void onSaveInstanceState(Bundle outState) {
ThreadUtils.assertOnUiThread();
outState.putBoolean(STATE_ENABLED, prefEnabled);
}
public void onRestoreInstanceState(Bundle savedInstanceState) {
ThreadUtils.assertOnUiThread();
if (savedInstanceState != null) {
prefEnabled = savedInstanceState.getBoolean(STATE_ENABLED);
}
}
public boolean isEnabled() {
ThreadUtils.assertOnUiThread();
if (forceDisabled) {
return false;
}
return prefEnabled && !accessibilityEnabled;
}
public void setAccessibilityEnabled(boolean enabled) {
ThreadUtils.assertOnUiThread();
if (accessibilityEnabled == enabled) {
return;
}
// Disable the dynamic toolbar when accessibility features are enabled,
// and re-read the preference when they're disabled.
accessibilityEnabled = enabled;
if (prefEnabled) {
triggerEnabledListener();
}
}
public void setVisible(boolean visible, VisibilityTransition transition) {
ThreadUtils.assertOnUiThread();
if (layerView == null) {
return;
}
// Don't hide the ActionBar/Toolbar, if it's pinned open by TextSelection.
if (visible == false &&
layerView.getDynamicToolbarAnimator().isPinnedBy(PinReason.ACTION_MODE)) {
return;
}
final boolean isImmediate = transition == VisibilityTransition.IMMEDIATE;
if (visible) {
layerView.getDynamicToolbarAnimator().showToolbar(isImmediate);
} else {
layerView.getDynamicToolbarAnimator().hideToolbar(isImmediate);
}
}
public void setTemporarilyVisible(boolean visible, VisibilityTransition transition) {
ThreadUtils.assertOnUiThread();
if (layerView == null) {
return;
}
if (visible == temporarilyVisible) {
// nothing to do
return;
}
temporarilyVisible = visible;
final boolean isImmediate = transition == VisibilityTransition.IMMEDIATE;
if (visible) {
layerView.getDynamicToolbarAnimator().showToolbar(isImmediate);
} else {
layerView.getDynamicToolbarAnimator().hideToolbar(isImmediate);
}
}
public void persistTemporaryVisibility() {
ThreadUtils.assertOnUiThread();
if (temporarilyVisible) {
temporarilyVisible = false;
setVisible(true, VisibilityTransition.IMMEDIATE);
}
}
public void setPinned(boolean pinned, PinReason reason) {
ThreadUtils.assertOnUiThread();
if (layerView == null) {
return;
}
layerView.getDynamicToolbarAnimator().setPinned(pinned, reason);
}
private void triggerEnabledListener() {
if (enabledChangedListener != null) {
enabledChangedListener.onEnabledChanged(isEnabled());
}
}
private class PrefHandler extends PrefHandlerBase {
@Override
public void prefValue(String pref, boolean value) {
if (value == prefEnabled) {
return;
}
prefEnabled = value;
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
// If accessibility is enabled, the dynamic toolbar is
// forced to be off.
if (!accessibilityEnabled) {
triggerEnabledListener();
}
}
});
}
}
}

View file

@ -0,0 +1,252 @@
/* -*- 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;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.database.Cursor;
import android.support.design.widget.Snackbar;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
/**
* A dialog that allows editing a bookmarks url, title, or keywords
* <p>
* Invoked by calling one of the {@link org.mozilla.gecko.EditBookmarkDialog#show(String)}
* methods.
*/
public class EditBookmarkDialog {
private final Context mContext;
public EditBookmarkDialog(Context context) {
mContext = context;
}
/**
* A private struct to make it easier to pass bookmark data across threads
*/
private class Bookmark {
final int id;
final String title;
final String url;
final String keyword;
public Bookmark(int aId, String aTitle, String aUrl, String aKeyword) {
id = aId;
title = aTitle;
url = aUrl;
keyword = aKeyword;
}
}
/**
* This text watcher to enable or disable the OK button if the dialog contains
* valid information. This class is overridden to do data checking on different fields.
* By itself, it always enables the button.
*
* Callers can also assign a paired partner to the TextWatcher, and callers will check
* that both are enabled before enabling the ok button.
*/
private class EditBookmarkTextWatcher implements TextWatcher {
// A stored reference to the dialog containing the text field being watched
protected AlertDialog mDialog;
// A stored text watcher to do the real verification of a field
protected EditBookmarkTextWatcher mPairedTextWatcher;
// Whether or not the ok button should be enabled.
protected boolean mEnabled = true;
public EditBookmarkTextWatcher(AlertDialog aDialog) {
mDialog = aDialog;
}
public void setPairedTextWatcher(EditBookmarkTextWatcher aTextWatcher) {
mPairedTextWatcher = aTextWatcher;
}
public boolean isEnabled() {
return mEnabled;
}
// Textwatcher interface
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// Disable if the we're disabled or the paired partner is disabled
boolean enabled = mEnabled && (mPairedTextWatcher == null || mPairedTextWatcher.isEnabled());
mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
}
@Override
public void afterTextChanged(Editable s) {}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
}
/**
* A version of the EditBookmarkTextWatcher for the url field of the dialog.
* Only checks if the field is empty or not.
*/
private class LocationTextWatcher extends EditBookmarkTextWatcher {
public LocationTextWatcher(AlertDialog aDialog) {
super(aDialog);
}
// Disables the ok button if the location field is empty.
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mEnabled = (s.toString().trim().length() > 0);
super.onTextChanged(s, start, before, count);
}
}
/**
* A version of the EditBookmarkTextWatcher for the keyword field of the dialog.
* Checks if the field has any (non leading or trailing) spaces.
*/
private class KeywordTextWatcher extends EditBookmarkTextWatcher {
public KeywordTextWatcher(AlertDialog aDialog) {
super(aDialog);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// Disable if the keyword contains spaces
mEnabled = (s.toString().trim().indexOf(' ') == -1);
super.onTextChanged(s, start, before, count);
}
}
/**
* Show the Edit bookmark dialog for a particular url. If the url is bookmarked multiple times
* this will just edit the first instance it finds.
*
* @param url The url of the bookmark to edit. The dialog will look up other information like the id,
* current title, or keywords associated with this url. If the url isn't bookmarked, the
* dialog will fail silently. If the url is bookmarked multiple times, this will only show
* information about the first it finds.
*/
public void show(final String url) {
final ContentResolver cr = mContext.getContentResolver();
final BrowserDB db = BrowserDB.from(mContext);
(new UIAsyncTask.WithoutParams<Bookmark>(ThreadUtils.getBackgroundHandler()) {
@Override
public Bookmark doInBackground() {
final Cursor cursor = db.getBookmarkForUrl(cr, url);
if (cursor == null) {
return null;
}
Bookmark bookmark = null;
try {
cursor.moveToFirst();
bookmark = new Bookmark(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE)),
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)),
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.KEYWORD)));
} finally {
cursor.close();
}
return bookmark;
}
@Override
public void onPostExecute(Bookmark bookmark) {
if (bookmark == null) {
return;
}
show(bookmark.id, bookmark.title, bookmark.url, bookmark.keyword);
}
}).execute();
}
/**
* Show the Edit bookmark dialog for a set of data. This will show the dialog whether
* a bookmark with this url exists or not, but the results will NOT be saved if the id
* is not a valid bookmark id.
*
* @param id The id of the bookmark to change. If there is no bookmark with this ID, the dialog
* will fail silently.
* @param title The initial title to show in the dialog
* @param url The initial url to show in the dialog
* @param keyword The initial keyword to show in the dialog
*/
public void show(final int id, final String title, final String url, final String keyword) {
final Context context = mContext;
AlertDialog.Builder editPrompt = new AlertDialog.Builder(context);
final View editView = LayoutInflater.from(context).inflate(R.layout.bookmark_edit, null);
editPrompt.setTitle(R.string.bookmark_edit_title);
editPrompt.setView(editView);
final EditText nameText = ((EditText) editView.findViewById(R.id.edit_bookmark_name));
final EditText locationText = ((EditText) editView.findViewById(R.id.edit_bookmark_location));
final EditText keywordText = ((EditText) editView.findViewById(R.id.edit_bookmark_keyword));
nameText.setText(title);
locationText.setText(url);
keywordText.setText(keyword);
final BrowserDB db = BrowserDB.from(mContext);
editPrompt.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
(new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
@Override
public Void doInBackground() {
String newUrl = locationText.getText().toString().trim();
String newKeyword = keywordText.getText().toString().trim();
db.updateBookmark(context.getContentResolver(), id, newUrl, nameText.getText().toString(), newKeyword);
return null;
}
@Override
public void onPostExecute(Void result) {
SnackbarBuilder.builder((Activity) context)
.message(R.string.bookmark_updated)
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
}
}).execute();
}
});
editPrompt.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
// do nothing
}
});
final AlertDialog dialog = editPrompt.create();
// Create our TextWatchers
LocationTextWatcher locationTextWatcher = new LocationTextWatcher(dialog);
KeywordTextWatcher keywordTextWatcher = new KeywordTextWatcher(dialog);
// Cross reference the TextWatchers
locationTextWatcher.setPairedTextWatcher(keywordTextWatcher);
keywordTextWatcher.setPairedTextWatcher(locationTextWatcher);
// Add the TextWatcher Listeners
locationText.addTextChangedListener(locationTextWatcher);
keywordText.addTextChangedListener(keywordTextWatcher);
dialog.show();
}
}

View file

@ -0,0 +1,119 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.content.Context;
import android.util.Log;
import android.text.TextUtils;
import com.keepsafe.switchboard.Preferences;
import com.keepsafe.switchboard.SwitchBoard;
import java.util.LinkedList;
import java.util.List;
/**
* This class should reflect the experiment names found in the Switchboard experiments config here:
* https://github.com/mozilla-services/switchboard-experiments
*/
public class Experiments {
private static final String LOGTAG = "GeckoExperiments";
// Show a system notification linking to a "What's New" page on app update.
public static final String WHATSNEW_NOTIFICATION = "whatsnew-notification";
// Subscribe to known, bookmarked sites and show a notification if new content is available.
public static final String CONTENT_NOTIFICATIONS_12HRS = "content-notifications-12hrs";
public static final String CONTENT_NOTIFICATIONS_8AM = "content-notifications-8am";
public static final String CONTENT_NOTIFICATIONS_5PM = "content-notifications-5pm";
// Onboarding: "Features and Story". These experiments are determined
// on the client, they are not part of the server config.
public static final String ONBOARDING3_A = "onboarding3-a"; // Control: No first run
public static final String ONBOARDING3_B = "onboarding3-b"; // 4 static Feature + 1 dynamic slides
public static final String ONBOARDING3_C = "onboarding3-c"; // Differentiating features slides
// Synchronizing the catalog of downloadable content from Kinto
public static final String DOWNLOAD_CONTENT_CATALOG_SYNC = "download-content-catalog-sync";
// Promotion for "Add to homescreen"
public static final String PROMOTE_ADD_TO_HOMESCREEN = "promote-add-to-homescreen";
public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
// Promotion to bookmark reader-view items after entering reader view three times (Bug 1247689)
public static final String TRIPLE_READERVIEW_BOOKMARK_PROMPT = "triple-readerview-bookmark-prompt";
// Only show origin in URL bar instead of full URL (Bug 1236431)
public static final String URLBAR_SHOW_ORIGIN_ONLY = "urlbar-show-origin-only";
// Show name of organization (EV cert) instead of full URL in URL bar (Bug 1249594).
public static final String URLBAR_SHOW_EV_CERT_OWNER = "urlbar-show-ev-cert-owner";
// Play HLS videos in a VideoView (Bug 1313391)
public static final String HLS_VIDEO_PLAYBACK = "hls-video-playback";
// Make new activity stream panel available (to replace top sites) (Bug 1313316)
public static final String ACTIVITY_STREAM = "activity-stream";
/**
* Returns if a user is in certain local experiment.
* @param experiment Name of experiment to look up
* @return returns value for experiment or false if experiment does not exist.
*/
public static boolean isInExperimentLocal(Context context, String experiment) {
if (SwitchBoard.isInBucket(context, 0, 20)) {
return Experiments.ONBOARDING3_A.equals(experiment);
} else if (SwitchBoard.isInBucket(context, 20, 60)) {
return Experiments.ONBOARDING3_B.equals(experiment);
} else if (SwitchBoard.isInBucket(context, 60, 100)) {
return Experiments.ONBOARDING3_C.equals(experiment);
} else {
return false;
}
}
/**
* Returns list of all active experiments, remote and local.
* @return List of experiment names Strings
*/
public static List<String> getActiveExperiments(Context c) {
final List<String> experiments = new LinkedList<>();
experiments.addAll(SwitchBoard.getActiveExperiments(c));
// Add onboarding version.
final String onboardingExperiment = GeckoSharedPrefs.forProfile(c).getString(Experiments.PREF_ONBOARDING_VERSION, null);
if (!TextUtils.isEmpty(onboardingExperiment)) {
experiments.add(onboardingExperiment);
}
return experiments;
}
/**
* Sets an override to force an experiment to be enabled or disabled. This value
* will be read and used before reading the switchboard server configuration.
*
* @param c Context
* @param experimentName Experiment name
* @param isEnabled Whether or not the experiment should be enabled
*/
public static void setOverride(Context c, String experimentName, boolean isEnabled) {
Log.d(LOGTAG, "setOverride: " + experimentName + " = " + isEnabled);
Preferences.setOverrideValue(c, experimentName, isEnabled);
}
/**
* Clears the override value for an experiment.
*
* @param c Context
* @param experimentName Experiment name
*/
public static void clearOverride(Context c, String experimentName) {
Log.d(LOGTAG, "clearOverride: " + experimentName);
Preferences.clearOverrideValue(c, experimentName);
}
}

View file

@ -0,0 +1,227 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.GeckoEventListener;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Environment;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class FilePicker implements GeckoEventListener {
private static final String LOGTAG = "GeckoFilePicker";
private static FilePicker sFilePicker;
private final Context context;
public interface ResultHandler {
public void gotFile(String filename);
}
public static void init(Context context) {
if (sFilePicker == null) {
sFilePicker = new FilePicker(context.getApplicationContext());
}
}
protected FilePicker(Context context) {
this.context = context;
EventDispatcher.getInstance().registerGeckoThreadListener(this, "FilePicker:Show");
}
@Override
public void handleMessage(String event, final JSONObject message) {
if (event.equals("FilePicker:Show")) {
String mimeType = "*/*";
final String mode = message.optString("mode");
final int tabId = message.optInt("tabId", -1);
final String title = message.optString("title");
if ("mimeType".equals(mode))
mimeType = message.optString("mimeType");
else if ("extension".equals(mode))
mimeType = GeckoAppShell.getMimeTypeFromExtensions(message.optString("extensions"));
showFilePickerAsync(title, mimeType, new ResultHandler() {
@Override
public void gotFile(String filename) {
try {
message.put("file", filename);
} catch (JSONException ex) {
Log.i(LOGTAG, "Can't add filename to message " + filename);
}
GeckoAppShell.notifyObservers("FilePicker:Result", message.toString());
}
}, tabId);
}
}
private void addActivities(Intent intent, HashMap<String, Intent> intents, HashMap<String, Intent> filters) {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> lri = pm.queryIntentActivities(intent, 0);
for (ResolveInfo ri : lri) {
ComponentName cn = new ComponentName(ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name);
if (filters != null && !filters.containsKey(cn.toString())) {
Intent rintent = new Intent(intent);
rintent.setComponent(cn);
intents.put(cn.toString(), rintent);
}
}
}
private Intent getIntent(String mimeType) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(mimeType);
intent.addCategory(Intent.CATEGORY_OPENABLE);
return intent;
}
private List<Intent> getIntentsForFilePicker(final String mimeType,
final FilePickerResultHandler fileHandler) {
// The base intent to use for the file picker. Even if this is an implicit intent, Android will
// still show a list of Activities that match this action/type.
Intent baseIntent;
// A HashMap of Activities the base intent will show in the chooser. This is used
// to filter activities from other intents so that we don't show duplicates.
HashMap<String, Intent> baseIntents = new HashMap<String, Intent>();
// A list of other activities to shwo in the picker (and the intents to launch them).
HashMap<String, Intent> intents = new HashMap<String, Intent> ();
if ("audio/*".equals(mimeType)) {
// For audio the only intent is the mimetype
baseIntent = getIntent(mimeType);
addActivities(baseIntent, baseIntents, null);
} else if ("image/*".equals(mimeType)) {
// For images the base is a capture intent
baseIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
baseIntent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
fileHandler.generateImageName())));
addActivities(baseIntent, baseIntents, null);
// We also add the mimetype intent
addActivities(getIntent(mimeType), intents, baseIntents);
} else if ("video/*".equals(mimeType)) {
// For videos the base is a capture intent
baseIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
addActivities(baseIntent, baseIntents, null);
// We also add the mimetype intent
addActivities(getIntent(mimeType), intents, baseIntents);
} else {
baseIntent = getIntent("*/*");
addActivities(baseIntent, baseIntents, null);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
fileHandler.generateImageName())));
addActivities(intent, intents, baseIntents);
intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
addActivities(intent, intents, baseIntents);
}
// If we didn't find any activities, we fall back to the */* mimetype intent
if (baseIntents.size() == 0 && intents.size() == 0) {
intents.clear();
baseIntent = getIntent("*/*");
addActivities(baseIntent, baseIntents, null);
}
ArrayList<Intent> vals = new ArrayList<Intent>(intents.values());
vals.add(0, baseIntent);
return vals;
}
private String getFilePickerTitle(String mimeType) {
if (mimeType.equals("audio/*")) {
return context.getString(R.string.filepicker_audio_title);
} else if (mimeType.equals("image/*")) {
return context.getString(R.string.filepicker_image_title);
} else if (mimeType.equals("video/*")) {
return context.getString(R.string.filepicker_video_title);
} else {
return context.getString(R.string.filepicker_title);
}
}
private interface IntentHandler {
public void gotIntent(Intent intent);
}
/* Gets an intent that can open a particular mimetype. Will show a prompt with a list
* of Activities that can handle the mietype. Asynchronously calls the handler when
* one of the intents is selected. If the caller passes in null for the handler, will still
* prompt for the activity, but will throw away the result.
*/
private void getFilePickerIntentAsync(String title,
final String mimeType,
final FilePickerResultHandler fileHandler,
final IntentHandler handler) {
List<Intent> intents = getIntentsForFilePicker(mimeType, fileHandler);
if (intents.size() == 0) {
Log.i(LOGTAG, "no activities for the file picker!");
handler.gotIntent(null);
return;
}
Intent base = intents.remove(0);
if (intents.size() == 0) {
handler.gotIntent(base);
return;
}
if (TextUtils.isEmpty(title)) {
title = getFilePickerTitle(mimeType);
}
Intent chooser = Intent.createChooser(base, title);
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[intents.size()]));
handler.gotIntent(chooser);
}
/* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
* sends the file returned to the passed in handler. If a null handler is passed in, will still
* pick and launch the file picker, but will throw away the result.
*/
protected void showFilePickerAsync(final String title, final String mimeType, final ResultHandler handler, final int tabId) {
final FilePickerResultHandler fileHandler = new FilePickerResultHandler(handler, context, tabId);
getFilePickerIntentAsync(title, mimeType, fileHandler, new IntentHandler() {
@Override
public void gotIntent(Intent intent) {
if (handler == null) {
return;
}
if (intent == null) {
handler.gotFile("");
return;
}
ActivityHandlerHelper.startIntent(intent, fileHandler);
}
});
}
}

View file

@ -0,0 +1,282 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Process;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.text.format.Time;
import android.util.Log;
class FilePickerResultHandler implements ActivityResultHandler {
private static final String LOGTAG = "GeckoFilePickerResultHandler";
private static final String UPLOADS_DIR = "uploads";
private final FilePicker.ResultHandler handler;
private final int tabId;
private final File cacheDir;
// this code is really hacky and doesn't belong anywhere so I'm putting it here for now
// until I can come up with a better solution.
private String mImageName = "";
/* Use this constructor to asynchronously listen for results */
public FilePickerResultHandler(final FilePicker.ResultHandler handler, final Context context, final int tabId) {
this.tabId = tabId;
this.cacheDir = new File(context.getCacheDir(), UPLOADS_DIR);
this.handler = handler;
}
void sendResult(String res) {
if (handler != null) {
handler.gotFile(res);
}
}
@Override
public void onActivityResult(int resultCode, Intent intent) {
if (resultCode != Activity.RESULT_OK) {
sendResult("");
return;
}
// Camera results won't return an Intent. Use the file name we passed to the original intent.
// In Android M, camera results return an empty Intent rather than null.
if (intent == null || (intent.getAction() == null && intent.getData() == null)) {
if (mImageName != null) {
File file = new File(Environment.getExternalStorageDirectory(), mImageName);
sendResult(file.getAbsolutePath());
} else {
sendResult("");
}
return;
}
Uri uri = intent.getData();
if (uri == null) {
sendResult("");
return;
}
// Some file pickers may return a file uri
if ("file".equals(uri.getScheme())) {
String path = uri.getPath();
sendResult(path == null ? "" : path);
return;
}
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
final LoaderManager lm = fa.getSupportLoaderManager();
// Finally, Video pickers and some file pickers may return a content provider.
final ContentResolver cr = fa.getContentResolver();
final Cursor cursor = cr.query(uri, new String[] { MediaStore.Video.Media.DATA }, null, null, null);
if (cursor != null) {
try {
// Try a query to make sure the expected columns exist
int index = cursor.getColumnIndex(MediaStore.Video.Media.DATA);
if (index >= 0) {
lm.initLoader(intent.hashCode(), null, new VideoLoaderCallbacks(uri));
return;
}
} catch (Exception ex) {
// We'll try a different loader below
} finally {
cursor.close();
}
}
lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId));
}
public String generateImageName() {
Time now = new Time();
now.setToNow();
mImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg";
return mImageName;
}
private class VideoLoaderCallbacks implements LoaderCallbacks<Cursor> {
final private Uri uri;
public VideoLoaderCallbacks(Uri uri) {
this.uri = uri;
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
return new CursorLoader(fa,
uri,
new String[] { MediaStore.Video.Media.DATA },
null, // selection
null, // selectionArgs
null); // sortOrder
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor.moveToFirst()) {
String res = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));
// Some pickers (the KitKat Documents one for instance) won't return a temporary file here.
// Fall back to the normal FileLoader if we didn't find anything.
if (TextUtils.isEmpty(res)) {
tryFileLoaderCallback();
return;
}
sendResult(res);
} else {
tryFileLoaderCallback();
}
}
private void tryFileLoaderCallback() {
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
final LoaderManager lm = fa.getSupportLoaderManager();
lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId));
}
@Override
public void onLoaderReset(Loader<Cursor> loader) { }
}
/**
* This class's only dependency on FilePickerResultHandler is sendResult.
*/
private class FileLoaderCallbacks implements LoaderCallbacks<Cursor>,
Tabs.OnTabsChangedListener {
private final Uri uri;
private final File cacheDir;
private final int tabId;
String tempFile;
public FileLoaderCallbacks(Uri uri, File cacheDir, int tabId) {
this.uri = uri;
this.cacheDir = cacheDir;
this.tabId = tabId;
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
return new CursorLoader(fa,
uri,
new String[] { OpenableColumns.DISPLAY_NAME },
null, // selection
null, // selectionArgs
null); // sortOrder
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor.moveToFirst()) {
String name = cursor.getString(0);
// tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
String fileName = "tmp_" + Process.myPid() + "-";
String fileExt;
int period;
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
final ContentResolver cr = fa.getContentResolver();
// Generate an extension if we don't already have one
if (name == null || (period = name.lastIndexOf('.')) == -1) {
String mimeType = cr.getType(uri);
fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType);
} else {
fileExt = name.substring(period);
fileName += name.substring(0, period);
}
// Now write the data to the temp file
FileOutputStream fos = null;
try {
cacheDir.mkdir();
File file = File.createTempFile(fileName, fileExt, cacheDir);
fos = new FileOutputStream(file);
InputStream is = cr.openInputStream(uri);
byte[] buf = new byte[4096];
int len = is.read(buf);
while (len != -1) {
fos.write(buf, 0, len);
len = is.read(buf);
}
fos.close();
is.close();
tempFile = file.getAbsolutePath();
sendResult((tempFile == null) ? "" : tempFile);
if (tabId > -1 && !TextUtils.isEmpty(tempFile)) {
Tabs.registerOnTabsChangedListener(this);
}
} catch (IOException ex) {
Log.i(LOGTAG, "Error writing file", ex);
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) { /* not much to do here */ }
}
}
} else {
sendResult("");
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) { }
/*Tabs.OnTabsChangedListener*/
// This cleans up our temp file. If it doesn't run, we just hope that Android
// will eventually does the cleanup for us.
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
if ((tab == null) || (tab.getId() != tabId)) {
return;
}
if (msg == Tabs.TabEvents.LOCATION_CHANGE ||
msg == Tabs.TabEvents.CLOSED) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
File f = new File(tempFile);
f.delete();
}
});
// Tabs' listener array is safe to modify during use: its
// iteration pattern is based on snapshots.
Tabs.unregisterOnTabsChangedListener(this);
}
}
}
}

View file

@ -0,0 +1,256 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONObject;
import android.content.Context;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import android.widget.TextView;
public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnClickListener, GeckoEventListener {
private static final String LOGTAG = "GeckoFindInPageBar";
private static final String REQUEST_ID = "FindInPageBar";
private final Context mContext;
private CustomEditText mFindText;
private TextView mStatusText;
private boolean mInflated;
public FindInPageBar(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
setFocusable(true);
}
public void inflateContent() {
LayoutInflater inflater = LayoutInflater.from(mContext);
View content = inflater.inflate(R.layout.find_in_page_content, this);
content.findViewById(R.id.find_prev).setOnClickListener(this);
content.findViewById(R.id.find_next).setOnClickListener(this);
content.findViewById(R.id.find_close).setOnClickListener(this);
// Capture clicks on the rest of the view to prevent them from
// leaking into other views positioned below.
content.setOnClickListener(this);
mFindText = (CustomEditText) content.findViewById(R.id.find_text);
mFindText.addTextChangedListener(this);
mFindText.setOnKeyPreImeListener(new CustomEditText.OnKeyPreImeListener() {
@Override
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
hide();
return true;
}
return false;
}
});
mStatusText = (TextView) content.findViewById(R.id.find_status);
mInflated = true;
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"FindInPage:MatchesCountResult",
"TextSelection:Data");
}
public void show() {
if (!mInflated)
inflateContent();
setVisibility(VISIBLE);
mFindText.requestFocus();
// handleMessage() receives response message and determines initial state of softInput
GeckoAppShell.notifyObservers("TextSelection:Get", REQUEST_ID);
GeckoAppShell.notifyObservers("FindInPage:Opened", null);
}
public void hide() {
if (!mInflated || getVisibility() == View.GONE) {
// There's nothing to hide yet.
return;
}
// Always clear the Find string, primarily for privacy.
mFindText.setText("");
// Only close the IMM if its EditText is the one with focus.
if (mFindText.isFocused()) {
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
}
// Close the FIPB / FindHelper state.
setVisibility(GONE);
GeckoAppShell.notifyObservers("FindInPage:Closed", null);
}
private InputMethodManager getInputMethodManager(View view) {
Context context = view.getContext();
return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
}
public void onDestroy() {
if (!mInflated) {
return;
}
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"FindInPage:MatchesCountResult",
"TextSelection:Data");
}
private void onMatchesCountResult(final int total, final int current, final int limit, final String searchString) {
if (total == -1) {
updateResult(Integer.toString(limit) + "+");
} else if (total > 0) {
updateResult(Integer.toString(current) + "/" + Integer.toString(total));
} else if (TextUtils.isEmpty(searchString)) {
updateResult("");
} else {
// We display 0/0, when there were no
// matches found, or if matching has been turned off by setting
// pref accessibility.typeaheadfind.matchesCountLimit to 0.
updateResult("0/0");
}
}
private void updateResult(final String statusText) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
mStatusText.setVisibility(statusText.isEmpty() ? View.GONE : View.VISIBLE);
mStatusText.setText(statusText);
}
});
}
// TextWatcher implementation
@Override
public void afterTextChanged(Editable s) {
sendRequestToFinderHelper("FindInPage:Find", s.toString());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// ignore
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// ignore
}
// View.OnClickListener implementation
@Override
public void onClick(View v) {
final int viewId = v.getId();
String extras = getResources().getResourceEntryName(viewId);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, extras);
if (viewId == R.id.find_prev) {
sendRequestToFinderHelper("FindInPage:Prev", mFindText.getText().toString());
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
return;
}
if (viewId == R.id.find_next) {
sendRequestToFinderHelper("FindInPage:Next", mFindText.getText().toString());
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
return;
}
if (viewId == R.id.find_close) {
hide();
}
}
// GeckoEventListener implementation
@Override
public void handleMessage(String event, JSONObject message) {
if (event.equals("FindInPage:MatchesCountResult")) {
onMatchesCountResult(message.optInt("total", 0),
message.optInt("current", 0),
message.optInt("limit", 0),
message.optString("searchString"));
return;
}
if (!event.equals("TextSelection:Data") || !REQUEST_ID.equals(message.optString("requestId"))) {
return;
}
final String text = message.optString("text");
// Populate an initial find string, virtual keyboard not required.
if (!TextUtils.isEmpty(text)) {
// Populate initial selection
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
mFindText.setText(text);
}
});
return;
}
// Show the virtual keyboard.
if (mFindText.hasWindowFocus()) {
getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
} else {
// showSoftInput won't work until after the window is focused.
mFindText.setOnWindowFocusChangeListener(new CustomEditText.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (!hasFocus)
return;
mFindText.setOnWindowFocusChangeListener(null);
getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
}
});
}
}
/**
* Request find operation, and update matchCount results (current count and total).
*/
private void sendRequestToFinderHelper(final String request, final String searchString) {
GeckoAppShell.sendRequestToGecko(new GeckoRequest(request, searchString) {
@Override
public void onResponse(NativeJSObject nativeJSObject) {
// We don't care about the return value, because `onMatchesCountResult`
// does the heavy lifting.
}
@Override
public void onError(NativeJSObject error) {
// Gecko didn't respond due to state change, javascript error, etc.
Log.d(LOGTAG, "No response from Gecko on request to match string: [" +
searchString + "]");
updateResult("");
}
});
}
}

View file

@ -0,0 +1,459 @@
/* -*- 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;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.gfx.FloatSize;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener;
import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener.OnDismissCallback;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
import android.widget.TextView;
import java.util.Arrays;
import java.util.Collection;
public class FormAssistPopup extends RelativeLayout implements GeckoEventListener {
private final Context mContext;
private final Animation mAnimation;
private ListView mAutoCompleteList;
private RelativeLayout mValidationMessage;
private TextView mValidationMessageText;
private ImageView mValidationMessageArrow;
private ImageView mValidationMessageArrowInverted;
private double mX;
private double mY;
private double mW;
private double mH;
private enum PopupType {
AUTOCOMPLETE,
VALIDATIONMESSAGE;
}
private PopupType mPopupType;
private static final int MAX_VISIBLE_ROWS = 5;
private static int sAutoCompleteMinWidth;
private static int sAutoCompleteRowHeight;
private static int sValidationMessageHeight;
private static int sValidationTextMarginTop;
private static LayoutParams sValidationTextLayoutNormal;
private static LayoutParams sValidationTextLayoutInverted;
private static final String LOGTAG = "GeckoFormAssistPopup";
// The blocklist is so short that ArrayList is probably cheaper than HashSet.
private static final Collection<String> sInputMethodBlocklist = Arrays.asList(
InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850
InputMethods.METHOD_OPENWNN_PLUS, // bug 768108
InputMethods.METHOD_SIMEJI, // bug 768108
InputMethods.METHOD_SWYPE, // bug 755909
InputMethods.METHOD_SWYPE_BETA // bug 755909
);
public FormAssistPopup(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
mAnimation.setDuration(75);
setFocusable(false);
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"FormAssist:AutoComplete",
"FormAssist:ValidationMessage",
"FormAssist:Hide");
}
void destroy() {
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"FormAssist:AutoComplete",
"FormAssist:ValidationMessage",
"FormAssist:Hide");
}
@Override
public void handleMessage(String event, JSONObject message) {
try {
if (event.equals("FormAssist:AutoComplete")) {
handleAutoCompleteMessage(message);
} else if (event.equals("FormAssist:ValidationMessage")) {
handleValidationMessage(message);
} else if (event.equals("FormAssist:Hide")) {
handleHideMessage(message);
}
} catch (Exception e) {
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
}
}
private void handleAutoCompleteMessage(JSONObject message) throws JSONException {
final JSONArray suggestions = message.getJSONArray("suggestions");
final JSONObject rect = message.getJSONObject("rect");
final boolean isEmpty = message.getBoolean("isEmpty");
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
showAutoCompleteSuggestions(suggestions, rect, isEmpty);
}
});
}
private void handleValidationMessage(JSONObject message) throws JSONException {
final String validationMessage = message.getString("validationMessage");
final JSONObject rect = message.getJSONObject("rect");
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
showValidationMessage(validationMessage, rect);
}
});
}
private void handleHideMessage(JSONObject message) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
hide();
}
});
}
private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect, boolean isEmpty) {
final String inputMethod = InputMethods.getCurrentInputMethod(mContext);
if (!isEmpty && sInputMethodBlocklist.contains(inputMethod)) {
// Don't display the form auto-complete popup after the user starts typing
// to avoid confusing somes IME. See bug 758820 and bug 632744.
hide();
return;
}
if (mAutoCompleteList == null) {
LayoutInflater inflater = LayoutInflater.from(mContext);
mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null);
mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parentView, View view, int position, long id) {
// Use the value stored with the autocomplete view, not the label text,
// since they can be different.
TextView textView = (TextView) view;
String value = (String) textView.getTag();
broadcastGeckoEvent("FormAssist:AutoComplete", value);
hide();
}
});
// Create a ListView-specific touch listener. ListViews are given special treatment because
// by default they handle touches for their list items... i.e. they're in charge of drawing
// the pressed state (the list selector), handling list item clicks, etc.
final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(mAutoCompleteList, new OnDismissCallback() {
@Override
public void onDismiss(ListView listView, final int position) {
// Use the value stored with the autocomplete view, not the label text,
// since they can be different.
AutoCompleteListAdapter adapter = (AutoCompleteListAdapter) listView.getAdapter();
Pair<String, String> item = adapter.getItem(position);
// Remove the item from form history.
broadcastGeckoEvent("FormAssist:Remove", item.second);
// Update the list
adapter.remove(item);
adapter.notifyDataSetChanged();
positionAndShowPopup();
}
});
mAutoCompleteList.setOnTouchListener(touchListener);
// Setting this scroll listener is required to ensure that during ListView scrolling,
// we don't look for swipes.
mAutoCompleteList.setOnScrollListener(touchListener.makeScrollListener());
// Setting this recycler listener is required to make sure animated views are reset.
mAutoCompleteList.setRecyclerListener(touchListener.makeRecyclerListener());
addView(mAutoCompleteList);
}
AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item);
adapter.populateSuggestionsList(suggestions);
mAutoCompleteList.setAdapter(adapter);
if (setGeckoPositionData(rect, true)) {
positionAndShowPopup();
}
}
private void showValidationMessage(String validationMessage, JSONObject rect) {
if (mValidationMessage == null) {
LayoutInflater inflater = LayoutInflater.from(mContext);
mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null);
addView(mValidationMessage);
mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text);
sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top));
sValidationTextLayoutNormal = new LayoutParams(mValidationMessageText.getLayoutParams());
sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0);
sValidationTextLayoutInverted = new LayoutParams((ViewGroup.MarginLayoutParams) sValidationTextLayoutNormal);
sValidationTextLayoutInverted.setMargins(0, 0, 0, 0);
mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow);
mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted);
}
mValidationMessageText.setText(validationMessage);
// We need to set the text as selected for the marquee text to work.
mValidationMessageText.setSelected(true);
if (setGeckoPositionData(rect, false)) {
positionAndShowPopup();
}
}
private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) {
try {
mX = rect.getDouble("x");
mY = rect.getDouble("y");
mW = rect.getDouble("w");
mH = rect.getDouble("h");
} catch (JSONException e) {
// Bail if we can't get the correct dimensions for the popup.
Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e);
return false;
}
mPopupType = (isAutoComplete ?
PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
return true;
}
private void positionAndShowPopup() {
positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics());
}
private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
ThreadUtils.assertOnUiThread();
// Don't show the form assist popup when using fullscreen VKB
InputMethodManager imm =
(InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm.isFullscreenMode()) {
return;
}
// Hide/show the appropriate popup contents
if (mAutoCompleteList != null) {
mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE);
}
if (mValidationMessage != null) {
mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE);
}
if (sAutoCompleteMinWidth == 0) {
Resources res = mContext.getResources();
sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width));
sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height));
sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height));
}
float zoom = aMetrics.zoomFactor;
// These values correspond to the input box for which we want to
// display the FormAssistPopup.
int left = (int) (mX * zoom - aMetrics.viewportRectLeft);
int top = (int) (mY * zoom - aMetrics.viewportRectTop + GeckoAppShell.getLayerView().getSurfaceTranslation());
int width = (int) (mW * zoom);
int height = (int) (mH * zoom);
int popupWidth = LayoutParams.MATCH_PARENT;
int popupLeft = left < 0 ? 0 : left;
FloatSize viewport = aMetrics.getSize();
// For autocomplete suggestions, if the input is smaller than the screen-width,
// shrink the popup's width. Otherwise, keep it as MATCH_PARENT.
if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) {
popupWidth = left < 0 ? left + width : width;
// Ensure the popup has a minimum width.
if (popupWidth < sAutoCompleteMinWidth) {
popupWidth = sAutoCompleteMinWidth;
// Move the popup to the left if there isn't enough room for it.
if ((popupLeft + popupWidth) > viewport.width) {
popupLeft = (int) (viewport.width - popupWidth);
}
}
}
int popupHeight;
if (mPopupType == PopupType.AUTOCOMPLETE) {
// Limit the amount of visible rows.
int rows = mAutoCompleteList.getAdapter().getCount();
if (rows > MAX_VISIBLE_ROWS) {
rows = MAX_VISIBLE_ROWS;
}
popupHeight = sAutoCompleteRowHeight * rows;
} else {
popupHeight = sValidationMessageHeight;
}
int popupTop = top + height;
if (mPopupType == PopupType.VALIDATIONMESSAGE) {
mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
mValidationMessageArrow.setVisibility(VISIBLE);
mValidationMessageArrowInverted.setVisibility(GONE);
}
// If the popup doesn't fit below the input box, shrink its height, or
// see if we can place it above the input instead.
if ((popupTop + popupHeight) > viewport.height) {
// Find where the maximum space is, and put the popup there.
if ((viewport.height - popupTop) > top) {
// Shrink the height to fit it below the input box.
popupHeight = (int) (viewport.height - popupTop);
} else {
if (popupHeight < top) {
// No shrinking needed to fit on top.
popupTop = (top - popupHeight);
} else {
// Shrink to available space on top.
popupTop = 0;
popupHeight = top;
}
if (mPopupType == PopupType.VALIDATIONMESSAGE) {
mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted);
mValidationMessageArrow.setVisibility(GONE);
mValidationMessageArrowInverted.setVisibility(VISIBLE);
}
}
}
LayoutParams layoutParams = new LayoutParams(popupWidth, popupHeight);
layoutParams.setMargins(popupLeft, popupTop, 0, 0);
setLayoutParams(layoutParams);
requestLayout();
if (!isShown()) {
setVisibility(VISIBLE);
startAnimation(mAnimation);
}
}
public void hide() {
if (isShown()) {
setVisibility(GONE);
broadcastGeckoEvent("FormAssist:Hidden", null);
}
}
void onTranslationChanged() {
ThreadUtils.assertOnUiThread();
if (!isShown()) {
return;
}
positionAndShowPopup();
}
void onMetricsChanged(final ImmutableViewportMetrics aMetrics) {
if (!isShown()) {
return;
}
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
positionAndShowPopup(aMetrics);
}
});
}
private static void broadcastGeckoEvent(String eventName, String eventData) {
GeckoAppShell.notifyObservers(eventName, eventData);
}
private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> {
private final LayoutInflater mInflater;
private final int mTextViewResourceId;
public AutoCompleteListAdapter(Context context, int textViewResourceId) {
super(context, textViewResourceId);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mTextViewResourceId = textViewResourceId;
}
// This method takes an array of autocomplete suggestions with label/value properties
// and adds label/value Pair objects to the array that backs the adapter.
public void populateSuggestionsList(JSONArray suggestions) {
try {
for (int i = 0; i < suggestions.length(); i++) {
JSONObject suggestion = suggestions.getJSONObject(i);
String label = suggestion.getString("label");
String value = suggestion.getString("value");
add(new Pair<String, String>(label, value));
}
} catch (JSONException e) {
Log.e(LOGTAG, "JSONException", e);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(mTextViewResourceId, null);
}
Pair<String, String> item = getItem(position);
TextView itemView = (TextView) convertView;
// Set the text with the suggestion label
itemView.setText(item.first);
// Set a tag with the suggestion value
itemView.setTag(item.second);
return convertView;
}
}
}

View file

@ -0,0 +1,100 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.content.ComponentName;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
public abstract class GeckoActivity extends AppCompatActivity implements GeckoActivityStatus {
// has this activity recently started another Gecko activity?
private boolean mGeckoActivityOpened;
/**
* Display any resources that show strings or encompass locale-specific
* representations.
*
* onLocaleReady must always be called on the UI thread.
*/
public void onLocaleReady(final String locale) {
}
@Override
public void onPause() {
super.onPause();
if (getApplication() instanceof GeckoApplication) {
((GeckoApplication) getApplication()).onActivityPause(this);
}
}
@Override
public void onResume() {
super.onResume();
if (getApplication() instanceof GeckoApplication) {
((GeckoApplication) getApplication()).onActivityResume(this);
mGeckoActivityOpened = false;
}
}
@Override
public void onCreate(android.os.Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (AppConstants.MOZ_ANDROID_ANR_REPORTER) {
ANRReporter.register(getApplicationContext());
}
}
@Override
public void onDestroy() {
if (AppConstants.MOZ_ANDROID_ANR_REPORTER) {
ANRReporter.unregister();
}
super.onDestroy();
}
@Override
public void startActivity(Intent intent) {
mGeckoActivityOpened = checkIfGeckoActivity(intent);
super.startActivity(intent);
}
@Override
public void startActivityForResult(Intent intent, int request) {
mGeckoActivityOpened = checkIfGeckoActivity(intent);
super.startActivityForResult(intent, request);
}
private static boolean checkIfGeckoActivity(Intent intent) {
// Whenever we call our own activity, the component and its package name is set.
// If we call an activity from another package, or an open intent (leaving android to resolve)
// component has a different package name or it is null.
ComponentName component = intent.getComponent();
return (component != null &&
AppConstants.ANDROID_PACKAGE_NAME.equals(component.getPackageName()));
}
@Override
public boolean isGeckoActivityOpened() {
return mGeckoActivityOpened;
}
public boolean isApplicationInBackground() {
return ((GeckoApplication) getApplication()).isApplicationInBackground();
}
@Override
public void onLowMemory() {
MemoryMonitor.getInstance().onLowMemory();
super.onLowMemory();
}
@Override
public void onTrimMemory(int level) {
MemoryMonitor.getInstance().onTrimMemory(level);
super.onTrimMemory(level);
}
}

View file

@ -0,0 +1,10 @@
/* 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;
public interface GeckoActivityStatus {
public boolean isGeckoActivityOpened();
public boolean isFinishing(); // typically from android.app.Activity
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,314 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.app.Application;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.dlc.DownloadContentService;
import org.mozilla.gecko.home.HomePanelsManager;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.mdns.MulticastDNSManager;
import org.mozilla.gecko.media.AudioFocusAgent;
import org.mozilla.gecko.notifications.NotificationClient;
import org.mozilla.gecko.notifications.NotificationHelper;
import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.ThreadUtils;
import java.io.File;
import java.lang.reflect.Method;
public class GeckoApplication extends Application
implements ContextGetter {
private static final String LOG_TAG = "GeckoApplication";
private static volatile GeckoApplication instance;
private boolean mInBackground;
private boolean mPausedGecko;
private LightweightTheme mLightweightTheme;
private RefWatcher mRefWatcher;
public GeckoApplication() {
super();
instance = this;
}
public static GeckoApplication get() {
return instance;
}
public static RefWatcher getRefWatcher(Context context) {
GeckoApplication app = (GeckoApplication) context.getApplicationContext();
return app.mRefWatcher;
}
public static void watchReference(Context context, Object object) {
if (context == null) {
return;
}
getRefWatcher(context).watch(object);
}
@Override
public Context getContext() {
return this;
}
@Override
public SharedPreferences getSharedPreferences() {
return GeckoSharedPrefs.forApp(this);
}
/**
* We need to do locale work here, because we need to intercept
* each hit to onConfigurationChanged.
*/
@Override
public void onConfigurationChanged(Configuration config) {
Log.d(LOG_TAG, "onConfigurationChanged: " + config.locale +
", background: " + mInBackground);
// Do nothing if we're in the background. It'll simply cause a loop
// (Bug 936756 Comment 11), and it's not necessary.
if (mInBackground) {
super.onConfigurationChanged(config);
return;
}
// Otherwise, correct the locale. This catches some cases that GeckoApp
// doesn't get a chance to.
try {
BrowserLocaleManager.getInstance().correctLocale(this, getResources(), config);
} catch (IllegalStateException ex) {
// GeckoApp hasn't started, so we have no ContextGetter in BrowserLocaleManager.
Log.w(LOG_TAG, "Couldn't correct locale.", ex);
}
super.onConfigurationChanged(config);
}
public void onActivityPause(GeckoActivityStatus activity) {
mInBackground = true;
if ((activity.isFinishing() == false) &&
(activity.isGeckoActivityOpened() == false)) {
// Notify Gecko that we are pausing; the cache service will be
// shutdown, closing the disk cache cleanly. If the android
// low memory killer subsequently kills us, the disk cache will
// be left in a consistent state, avoiding costly cleanup and
// re-creation.
GeckoThread.onPause();
mPausedGecko = true;
final BrowserDB db = BrowserDB.from(this);
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
db.expireHistory(getContentResolver(), BrowserContract.ExpirePriority.NORMAL);
}
});
}
GeckoNetworkManager.getInstance().stop();
}
public void onActivityResume(GeckoActivityStatus activity) {
if (mPausedGecko) {
GeckoThread.onResume();
mPausedGecko = false;
}
GeckoBatteryManager.getInstance().start(this);
GeckoNetworkManager.getInstance().start(this);
mInBackground = false;
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
AppConstants.maybeInstallMultiDex(base);
}
@Override
public void onCreate() {
Log.i(LOG_TAG, "zerdatime " + SystemClock.uptimeMillis() + " - Fennec application start");
mRefWatcher = LeakCanary.install(this);
final Context context = getApplicationContext();
GeckoAppShell.setApplicationContext(context);
HardwareUtils.init(context);
Clipboard.init(context);
FilePicker.init(context);
DownloadsIntegration.init();
HomePanelsManager.getInstance().init(context);
GlobalPageMetadata.getInstance().init();
// We need to set the notification client before launching Gecko, since Gecko could start
// sending notifications immediately after startup, which we don't want to lose/crash on.
GeckoAppShell.setNotificationListener(new NotificationClient(context));
// This getInstance call will force initialization of the NotificationHelper, but does nothing with the result
NotificationHelper.getInstance(context).init();
MulticastDNSManager.getInstance(context).init();
GeckoService.register();
EventDispatcher.getInstance().registerBackgroundThreadListener(new EventListener(),
"Profile:Create");
super.onCreate();
}
public void onDelayedStartup() {
if (AppConstants.MOZ_ANDROID_GCM) {
// TODO: only run in main process.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// It's fine to throw GCM initialization onto a background thread; the registration process requires
// network access, so is naturally asynchronous. This, of course, races against Gecko page load of
// content requiring GCM-backed services, like Web Push. There's nothing to be done here.
try {
final Class<?> clazz = Class.forName("org.mozilla.gecko.push.PushService");
final Method onCreate = clazz.getMethod("onCreate", Context.class);
onCreate.invoke(null, getApplicationContext()); // Method is static.
} catch (Exception e) {
Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
return;
}
}
});
}
if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
DownloadContentService.startStudy(this);
}
GeckoAccessibility.setAccessibilityManagerListeners(this);
AudioFocusAgent.getInstance().attachToContext(this);
}
private class EventListener implements BundleEventListener
{
private void onProfileCreate(final String name, final String path) {
// Add everything when we're done loading the distribution.
final Context context = GeckoApplication.this;
final GeckoProfile profile = GeckoProfile.get(context, name);
final Distribution distribution = Distribution.getInstance(context);
distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
@Override
public void distributionNotFound() {
this.distributionFound(null);
}
@Override
public void distributionFound(final Distribution distribution) {
Log.d(LOG_TAG, "Running post-distribution task: bookmarks.");
// Because we are running in the background, we want to synchronize on the
// GeckoProfile instance so that we don't race with main thread operations
// such as locking/unlocking/removing the profile.
synchronized (profile.getLock()) {
distributionFoundLocked(distribution);
}
}
@Override
public void distributionArrivedLate(final Distribution distribution) {
Log.d(LOG_TAG, "Running late distribution task: bookmarks.");
// Recover as best we can.
synchronized (profile.getLock()) {
distributionArrivedLateLocked(distribution);
}
}
private void distributionFoundLocked(final Distribution distribution) {
// Skip initialization if the profile directory has been removed.
if (!(new File(path)).exists()) {
return;
}
final ContentResolver cr = context.getContentResolver();
final LocalBrowserDB db = new LocalBrowserDB(profile.getName());
// We pass the number of added bookmarks to ensure that the
// indices of the distribution and default bookmarks are
// contiguous. Because there are always at least as many
// bookmarks as there are favicons, we can also guarantee that
// the favicon IDs won't overlap.
final int offset = distribution == null ? 0 :
db.addDistributionBookmarks(cr, distribution, 0);
db.addDefaultBookmarks(context, cr, offset);
Log.d(LOG_TAG, "Running post-distribution task: android preferences.");
DistroSharedPrefsImport.importPreferences(context, distribution);
}
private void distributionArrivedLateLocked(final Distribution distribution) {
// Skip initialization if the profile directory has been removed.
if (!(new File(path)).exists()) {
return;
}
final ContentResolver cr = context.getContentResolver();
final LocalBrowserDB db = new LocalBrowserDB(profile.getName());
// We assume we've been called very soon after startup, and so our offset
// into "Mobile Bookmarks" is the number of bookmarks in the DB.
final int offset = db.getCount(cr, "bookmarks");
db.addDistributionBookmarks(cr, distribution, offset);
Log.d(LOG_TAG, "Running late distribution task: android preferences.");
DistroSharedPrefsImport.importPreferences(context, distribution);
}
});
}
@Override // BundleEventListener
public void handleMessage(final String event, final Bundle message,
final EventCallback callback) {
if ("Profile:Create".equals(event)) {
onProfileCreate(message.getCharSequence("name").toString(),
message.getCharSequence("path").toString());
}
}
}
public boolean isApplicationInBackground() {
return mInBackground;
}
public LightweightTheme getLightweightTheme() {
return mLightweightTheme;
}
public void prepareLightweightTheme() {
mLightweightTheme = new LightweightTheme(this);
}
}

View file

@ -0,0 +1,211 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;
import org.mozilla.gecko.annotation.WrapForJNI;
import java.lang.Thread;
import java.util.Set;
public class GeckoJavaSampler {
private static final String LOGTAG = "JavaSampler";
private static Thread sSamplingThread;
private static SamplingThread sSamplingRunnable;
private static Thread sMainThread;
// Use the same timer primitive as the profiler
// to get a perfect sample syncing.
@WrapForJNI
private static native double getProfilerTime();
private static class Sample {
public Frame[] mFrames;
public double mTime;
public long mJavaTime; // non-zero if Android system time is used
public Sample(StackTraceElement[] aStack) {
mFrames = new Frame[aStack.length];
if (GeckoThread.isStateAtLeast(GeckoThread.State.LIBS_READY)) {
mTime = getProfilerTime();
}
if (mTime == 0.0d) {
// getProfilerTime is not available yet; either libs are not loaded,
// or profiling hasn't started on the Gecko side yet
mJavaTime = SystemClock.elapsedRealtime();
}
for (int i = 0; i < aStack.length; i++) {
mFrames[aStack.length - 1 - i] = new Frame();
mFrames[aStack.length - 1 - i].fileName = aStack[i].getFileName();
mFrames[aStack.length - 1 - i].lineNo = aStack[i].getLineNumber();
mFrames[aStack.length - 1 - i].methodName = aStack[i].getMethodName();
mFrames[aStack.length - 1 - i].className = aStack[i].getClassName();
}
}
}
private static class Frame {
public String fileName;
public int lineNo;
public String methodName;
public String className;
}
private static class SamplingThread implements Runnable {
private final int mInterval;
private final int mSampleCount;
private boolean mPauseSampler;
private boolean mStopSampler;
private final SparseArray<Sample[]> mSamples = new SparseArray<Sample[]>();
private int mSamplePos;
public SamplingThread(final int aInterval, final int aSampleCount) {
// If we sample faster then 10ms we get to many missed samples
mInterval = Math.max(10, aInterval);
mSampleCount = aSampleCount;
}
@Override
public void run() {
synchronized (GeckoJavaSampler.class) {
mSamples.put(0, new Sample[mSampleCount]);
mSamplePos = 0;
// Find the main thread
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (Thread t : threadSet) {
if (t.getName().compareToIgnoreCase("main") == 0) {
sMainThread = t;
break;
}
}
if (sMainThread == null) {
Log.e(LOGTAG, "Main thread not found");
return;
}
}
while (true) {
try {
Thread.sleep(mInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (GeckoJavaSampler.class) {
if (!mPauseSampler) {
StackTraceElement[] bt = sMainThread.getStackTrace();
mSamples.get(0)[mSamplePos] = new Sample(bt);
mSamplePos = (mSamplePos + 1) % mSamples.get(0).length;
}
if (mStopSampler) {
break;
}
}
}
}
private Sample getSample(int aThreadId, int aSampleId) {
if (aThreadId < mSamples.size() && aSampleId < mSamples.get(aThreadId).length &&
mSamples.get(aThreadId)[aSampleId] != null) {
int startPos = 0;
if (mSamples.get(aThreadId)[mSamplePos] != null) {
startPos = mSamplePos;
}
int readPos = (startPos + aSampleId) % mSamples.get(aThreadId).length;
return mSamples.get(aThreadId)[readPos];
}
return null;
}
}
@WrapForJNI
public synchronized static String getThreadName(int aThreadId) {
if (aThreadId == 0 && sMainThread != null) {
return sMainThread.getName();
}
return null;
}
private synchronized static Sample getSample(int aThreadId, int aSampleId) {
return sSamplingRunnable.getSample(aThreadId, aSampleId);
}
@WrapForJNI
public synchronized static double getSampleTime(int aThreadId, int aSampleId) {
Sample sample = getSample(aThreadId, aSampleId);
if (sample != null) {
if (sample.mJavaTime != 0) {
return (sample.mJavaTime -
SystemClock.elapsedRealtime()) + getProfilerTime();
}
System.out.println("Sample: " + sample.mTime);
return sample.mTime;
}
return 0;
}
@WrapForJNI
public synchronized static String getFrameName(int aThreadId, int aSampleId, int aFrameId) {
Sample sample = getSample(aThreadId, aSampleId);
if (sample != null && aFrameId < sample.mFrames.length) {
Frame frame = sample.mFrames[aFrameId];
if (frame == null) {
return null;
}
return frame.className + "." + frame.methodName + "()";
}
return null;
}
@WrapForJNI
public static void start(int aInterval, int aSamples) {
synchronized (GeckoJavaSampler.class) {
if (sSamplingRunnable != null) {
return;
}
sSamplingRunnable = new SamplingThread(aInterval, aSamples);
sSamplingThread = new Thread(sSamplingRunnable, "Java Sampler");
sSamplingThread.start();
}
}
@WrapForJNI
public static void pause() {
synchronized (GeckoJavaSampler.class) {
sSamplingRunnable.mPauseSampler = true;
}
}
@WrapForJNI
public static void unpause() {
synchronized (GeckoJavaSampler.class) {
sSamplingRunnable.mPauseSampler = false;
}
}
@WrapForJNI
public static void stop() {
synchronized (GeckoJavaSampler.class) {
if (sSamplingThread == null) {
return;
}
sSamplingRunnable.mStopSampler = true;
try {
sSamplingThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
sSamplingThread = null;
sSamplingRunnable = null;
}
}
}

View file

@ -0,0 +1,27 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.json.JSONObject;
import org.mozilla.gecko.util.EventCallback;
/**
* Wrapper for MediaRouter types supported by Android, such as Chromecast, Miracast, etc.
*/
interface GeckoMediaPlayer {
/**
* Can return null.
*/
JSONObject toJSON();
void load(String title, String url, String type, EventCallback callback);
void play(EventCallback callback);
void pause(EventCallback callback);
void stop(EventCallback callback);
void start(EventCallback callback);
void end(EventCallback callback);
void mirror(EventCallback callback);
void message(String message, EventCallback callback);
}

View file

@ -0,0 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class GeckoMessageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (GeckoApp.ACTION_INIT_PW.equals(action)) {
GeckoAppShell.notifyObservers("Passwords:Init", null);
}
}
}

View file

@ -0,0 +1,22 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.json.JSONObject;
import org.mozilla.gecko.util.EventCallback;
/**
* Wrapper for MediaRouter types supported by Android to use for
* Presentation API, such as Chromecast, Miracast, etc.
*/
interface GeckoPresentationDisplay {
/**
* Can return null.
*/
JSONObject toJSON();
void start(EventCallback callback);
void stop(EventCallback callback);
}

View file

@ -0,0 +1,149 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.io.File;
import java.util.Map;
import java.util.Map.Entry;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.db.BrowserContract;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.Log;
/**
* This is not a per-profile provider. This provider allows read-only,
* restricted access to certain attributes of Fennec profiles.
*/
public class GeckoProfilesProvider extends ContentProvider {
private static final String LOG_TAG = "GeckoProfilesProvider";
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
private static final int PROFILES = 100;
private static final int PROFILES_NAME = 101;
private static final int PROFILES_DEFAULT = 200;
private static final String[] DEFAULT_ARGS = {
BrowserContract.Profiles.NAME,
BrowserContract.Profiles.PATH,
};
static {
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles", PROFILES);
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles/*", PROFILES_NAME);
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "default", PROFILES_DEFAULT);
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public boolean onCreate() {
// Successfully loaded.
return true;
}
private String[] profileValues(final String name, final String path, int len, int nameIndex, int pathIndex) {
final String[] values = new String[len];
if (nameIndex >= 0) {
values[nameIndex] = name;
}
if (pathIndex >= 0) {
values[pathIndex] = path;
}
return values;
}
protected void addRowForProfile(final MatrixCursor cursor, final int len, final int nameIndex, final int pathIndex, final String name, final String path) {
if (path == null || name == null) {
return;
}
cursor.addRow(profileValues(name, path, len, nameIndex, pathIndex));
}
protected Cursor getCursorForProfiles(final String[] args, Map<String, String> profiles) {
// Compute the projection.
int nameIndex = -1;
int pathIndex = -1;
for (int i = 0; i < args.length; ++i) {
if (BrowserContract.Profiles.NAME.equals(args[i])) {
nameIndex = i;
} else if (BrowserContract.Profiles.PATH.equals(args[i])) {
pathIndex = i;
}
}
final MatrixCursor cursor = new MatrixCursor(args);
for (Entry<String, String> entry : profiles.entrySet()) {
addRowForProfile(cursor, args.length, nameIndex, pathIndex, entry.getKey(), entry.getValue());
}
return cursor;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
final String[] args = (projection == null) ? DEFAULT_ARGS : projection;
final File mozillaDir;
try {
mozillaDir = GeckoProfileDirectories.getMozillaDirectory(getContext());
} catch (NoMozillaDirectoryException e) {
Log.d(LOG_TAG, "No Mozilla directory; cannot query for profiles. Assuming there are none.");
return new MatrixCursor(projection);
}
final Map<String, String> matchingProfiles;
final int match = URI_MATCHER.match(uri);
switch (match) {
case PROFILES:
// Return all profiles.
matchingProfiles = GeckoProfileDirectories.getAllProfiles(mozillaDir);
break;
case PROFILES_NAME:
// Return data about the specified profile.
final String name = uri.getLastPathSegment();
matchingProfiles = GeckoProfileDirectories.getProfilesNamed(mozillaDir,
name);
break;
case PROFILES_DEFAULT:
matchingProfiles = GeckoProfileDirectories.getDefaultProfile(mozillaDir);
break;
default:
throw new UnsupportedOperationException("Unknown query URI " + uri);
}
return getCursorForProfiles(args, matchingProfiles);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new IllegalStateException("Inserts not supported.");
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new IllegalStateException("Deletes not supported.");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new IllegalStateException("Updates not supported.");
}
}

View file

@ -0,0 +1,236 @@
/* -*- 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;
import android.app.AlarmManager;
import android.app.Service;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.io.File;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.EventCallback;
public class GeckoService extends Service {
private static final String LOGTAG = "GeckoService";
private static final boolean DEBUG = false;
private static final String INTENT_PROFILE_NAME = "org.mozilla.gecko.intent.PROFILE_NAME";
private static final String INTENT_PROFILE_DIR = "org.mozilla.gecko.intent.PROFILE_DIR";
private static final String INTENT_ACTION_UPDATE_ADDONS = "update-addons";
private static final String INTENT_ACTION_CREATE_SERVICES = "create-services";
private static final String INTENT_SERVICE_CATEGORY = "category";
private static final String INTENT_SERVICE_DATA = "data";
private static class EventListener implements NativeEventListener {
@Override // NativeEventListener
public void handleMessage(final String event,
final NativeJSObject message,
final EventCallback callback) {
final Context context = GeckoAppShell.getApplicationContext();
switch (event) {
case "Gecko:ScheduleRun":
if (DEBUG) {
Log.d(LOGTAG, "Scheduling " + message.getString("action") +
" @ " + message.getInt("interval") + "ms");
}
final Intent intent = getIntentForAction(context, message.getString("action"));
final PendingIntent pendingIntent = PendingIntent.getService(
context, /* requestCode */ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
final AlarmManager am = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);
// Cancel any previous alarm and schedule a new one.
am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
message.getInt("trigger"),
message.getInt("interval"),
pendingIntent);
break;
default:
throw new UnsupportedOperationException(event);
}
}
}
private static final EventListener EVENT_LISTENER = new EventListener();
public static void register() {
if (DEBUG) {
Log.d(LOGTAG, "Registered listener");
}
EventDispatcher.getInstance().registerGeckoThreadListener(EVENT_LISTENER,
"Gecko:ScheduleRun");
}
public static void unregister() {
if (DEBUG) {
Log.d(LOGTAG, "Unregistered listener");
}
EventDispatcher.getInstance().unregisterGeckoThreadListener(EVENT_LISTENER,
"Gecko:ScheduleRun");
}
@Override // Service
public void onCreate() {
GeckoAppShell.ensureCrashHandling();
GeckoThread.onResume();
super.onCreate();
if (DEBUG) {
Log.d(LOGTAG, "Created");
}
}
@Override // Service
public void onDestroy() {
GeckoThread.onPause();
// We want to block here if we can, so we don't get killed when Gecko is in the
// middle of handling onPause().
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
GeckoThread.waitOnGecko();
}
if (DEBUG) {
Log.d(LOGTAG, "Destroyed");
}
super.onDestroy();
}
private static Intent getIntentForAction(final Context context, final String action) {
final Intent intent = new Intent(action, /* uri */ null, context, GeckoService.class);
final GeckoProfile profile = GeckoThread.getActiveProfile();
if (profile != null) {
setIntentProfile(intent, profile.getName(), profile.getDir().getAbsolutePath());
}
return intent;
}
public static Intent getIntentToCreateServices(final Context context, final String category, final String data) {
final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES);
intent.putExtra(INTENT_SERVICE_CATEGORY, category);
intent.putExtra(INTENT_SERVICE_DATA, data);
return intent;
}
public static Intent getIntentToCreateServices(final Context context, final String category) {
return getIntentToCreateServices(context, category, /* data */ null);
}
public static void setIntentProfile(final Intent intent, final String profileName,
final String profileDir) {
intent.putExtra(INTENT_PROFILE_NAME, profileName);
intent.putExtra(INTENT_PROFILE_DIR, profileDir);
}
private int handleIntent(final Intent intent, final int startId) {
if (DEBUG) {
Log.d(LOGTAG, "Handling " + intent.getAction());
}
final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
if (profileName == null) {
throw new IllegalArgumentException("Intent must specify profile.");
}
if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
profileDir != null ? new File(profileDir) : null)) {
Log.w(LOGTAG, "Ignoring due to profile mismatch: " +
profileName + " [" + profileDir + ']');
final GeckoProfile profile = GeckoThread.getActiveProfile();
if (profile != null) {
Log.w(LOGTAG, "Current profile is " + profile.getName() +
" [" + profile.getDir().getAbsolutePath() + ']');
}
stopSelf(startId);
return Service.START_NOT_STICKY;
}
GeckoThread.launch();
switch (intent.getAction()) {
case INTENT_ACTION_UPDATE_ADDONS:
// Run the add-on update service. Because the service is automatically invoked
// when loading Gecko, we don't have to do anything else here.
break;
case INTENT_ACTION_CREATE_SERVICES:
final String category = intent.getStringExtra(INTENT_SERVICE_CATEGORY);
final String data = intent.getStringExtra(INTENT_SERVICE_DATA);
if (category == null) {
break;
}
GeckoThread.createServices(category, data);
break;
default:
Log.w(LOGTAG, "Unknown request: " + intent);
}
stopSelf(startId);
return Service.START_NOT_STICKY;
}
@Override // Service
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent == null) {
return Service.START_NOT_STICKY;
}
try {
return handleIntent(intent, startId);
} catch (final Throwable e) {
Log.e(LOGTAG, "Cannot handle intent: " + intent, e);
return Service.START_NOT_STICKY;
}
}
@Override // Service
public IBinder onBind(final Intent intent) {
return null;
}
public static void startGecko(final GeckoProfile profile, final String args, final Context context) {
if (GeckoThread.isLaunched()) {
if (DEBUG) {
Log.v(LOGTAG, "already launched");
}
return;
}
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
GeckoAppShell.ensureCrashHandling();
GeckoAppShell.setApplicationContext(context);
GeckoThread.onResume();
GeckoThread.init(profile, args, null, false);
GeckoThread.launch();
if (DEBUG) {
Log.v(LOGTAG, "warmed up (launched)");
}
}
});
}
}

View file

@ -0,0 +1,25 @@
/* -*- 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;
import org.mozilla.gecko.updater.UpdateServiceHelper;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class GeckoUpdateReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent) {
if (UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT.equals(intent.getAction())) {
String result = intent.getStringExtra("result");
if (GeckoAppShell.getGeckoInterface() != null && result != null) {
GeckoAppShell.getGeckoInterface().notifyCheckUpdateResult(result);
}
}
}
}

View file

@ -0,0 +1,178 @@
/* -*- 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;
import java.lang.ref.SoftReference;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.reader.ReaderModeUtils;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
class GlobalHistory {
private static final String LOGTAG = "GeckoGlobalHistory";
public static final String EVENT_URI_AVAILABLE_IN_HISTORY = "URI_INSERTED_TO_HISTORY";
public static final String EVENT_PARAM_URI = "uri";
private static final String TELEMETRY_HISTOGRAM_ADD = "FENNEC_GLOBALHISTORY_ADD_MS";
private static final String TELEMETRY_HISTOGRAM_UPDATE = "FENNEC_GLOBALHISTORY_UPDATE_MS";
private static final String TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK = "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS";
private static final GlobalHistory sInstance = new GlobalHistory();
static GlobalHistory getInstance() {
return sInstance;
}
// this is the delay between receiving a URI check request and processing it.
// this allows batching together multiple requests and processing them together,
// which is more efficient.
private static final long BATCHING_DELAY_MS = 100;
private final Handler mHandler; // a background thread on which we can process requests
// Note: These fields are accessed through the NotificationRunnable inner class.
final Queue<String> mPendingUris; // URIs that need to be checked
SoftReference<Set<String>> mVisitedCache; // cache of the visited URI list
boolean mProcessing; // = false // whether or not the runnable is queued/working
private class NotifierRunnable implements Runnable {
private final ContentResolver mContentResolver;
private final BrowserDB mDB;
public NotifierRunnable(final Context context) {
mContentResolver = context.getContentResolver();
mDB = BrowserDB.from(context);
}
@Override
public void run() {
Set<String> visitedSet = mVisitedCache.get();
if (visitedSet == null) {
// The cache was wiped. Repopulate it.
Log.w(LOGTAG, "Rebuilding visited link set...");
final long start = SystemClock.uptimeMillis();
final Cursor c = mDB.getAllVisitedHistory(mContentResolver);
if (c == null) {
return;
}
try {
visitedSet = new HashSet<String>();
if (c.moveToFirst()) {
do {
visitedSet.add(c.getString(0));
} while (c.moveToNext());
}
mVisitedCache = new SoftReference<Set<String>>(visitedSet);
final long end = SystemClock.uptimeMillis();
final long took = end - start;
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
} finally {
c.close();
}
}
// This runs on the same handler thread as the checkUriVisited code,
// so no synchronization is needed.
while (true) {
final String uri = mPendingUris.poll();
if (uri == null) {
break;
}
if (visitedSet.contains(uri)) {
GeckoAppShell.notifyUriVisited(uri);
}
}
mProcessing = false;
}
};
private GlobalHistory() {
mHandler = ThreadUtils.getBackgroundHandler();
mPendingUris = new LinkedList<String>();
mVisitedCache = new SoftReference<Set<String>>(null);
}
public void addToGeckoOnly(String uri) {
Set<String> visitedSet = mVisitedCache.get();
if (visitedSet != null) {
visitedSet.add(uri);
}
GeckoAppShell.notifyUriVisited(uri);
}
public void add(final Context context, final BrowserDB db, String uri) {
ThreadUtils.assertOnBackgroundThread();
final long start = SystemClock.uptimeMillis();
// stripAboutReaderUrl only removes about:reader if present, in all other cases the original string is returned
final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri);
db.updateVisitedHistory(context.getContentResolver(), uriToStore);
final long end = SystemClock.uptimeMillis();
final long took = end - start;
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
addToGeckoOnly(uriToStore);
dispatchUriAvailableMessage(uri);
}
@SuppressWarnings("static-method")
public void update(final ContentResolver cr, final BrowserDB db, String uri, String title) {
ThreadUtils.assertOnBackgroundThread();
final long start = SystemClock.uptimeMillis();
final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri);
db.updateHistoryTitle(cr, uriToStore, title);
final long end = SystemClock.uptimeMillis();
final long took = end - start;
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
}
public void checkUriVisited(final String uri) {
final String storedURI = ReaderModeUtils.stripAboutReaderUrl(uri);
final NotifierRunnable runnable = new NotifierRunnable(GeckoAppShell.getContext());
mHandler.post(new Runnable() {
@Override
public void run() {
// this runs on the same handler thread as the processing loop,
// so no synchronization needed
mPendingUris.add(storedURI);
if (mProcessing) {
// there's already a runnable queued up or working away, so
// no need to post another
return;
}
mProcessing = true;
mHandler.postDelayed(runnable, BATCHING_DELAY_MS);
}
});
}
private void dispatchUriAvailableMessage(String uri) {
final Bundle message = new Bundle();
message.putString(EVENT_PARAM_URI, uri);
EventDispatcher.getInstance().dispatch(EVENT_URI_AVAILABLE_IN_HISTORY, message);
}
}

View file

@ -0,0 +1,182 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.content.ContentProviderClient;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.ThreadUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Provides access to metadata information about websites.
*
* While storing, in case of timing issues preventing us from looking up History GUID by a given uri,
* we queue up metadata and wait for GlobalHistory to let us know history record is now available.
*
* TODO Bug 1313515: selection of metadata for a given uri/history_GUID
*
* @author grisha
*/
/* package-local */ class GlobalPageMetadata implements BundleEventListener {
private static final String LOG_TAG = "GeckoGlobalPageMetadata";
private static final GlobalPageMetadata instance = new GlobalPageMetadata();
private static final String KEY_HAS_IMAGE = "hasImage";
private static final String KEY_METADATA_JSON = "metadataJSON";
private static final int MAX_METADATA_QUEUE_SIZE = 15;
private final Map<String, Bundle> queuedMetadata = Collections.synchronizedMap(new LimitedLinkedHashMap<String, Bundle>());
public static GlobalPageMetadata getInstance() {
return instance;
}
private static class LimitedLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = 6359725112736360244L;
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
if (size() > MAX_METADATA_QUEUE_SIZE) {
Log.w(LOG_TAG, "Page metadata queue is full. Dropping oldest metadata.");
return true;
}
return false;
}
}
private GlobalPageMetadata() {}
public void init() {
EventDispatcher
.getInstance()
.registerBackgroundThreadListener(this, GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY);
}
public void add(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) {
ThreadUtils.assertOnBackgroundThread();
// NB: Other than checking that JSON is valid and trimming it,
// we do not process metadataJSON in any way, trusting our source.
doAddOrQueue(db, contentProviderClient, uri, hasImage, metadataJSON);
}
@VisibleForTesting
/*package-local */ void doAddOrQueue(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) {
final String preparedMetadataJSON;
try {
preparedMetadataJSON = prepareJSON(metadataJSON);
} catch (JSONException e) {
Log.e(LOG_TAG, "Couldn't process metadata JSON", e);
return;
}
// Don't bother queuing this if deletions fails to find a corresponding history record.
// If we can't delete metadata because it didn't exist yet, that's OK.
if (preparedMetadataJSON.equals("{}")) {
final int deleted = db.deletePageMetadata(contentProviderClient, uri);
// We could delete none if history record for uri isn't present.
// We must delete one if history record for uri is present.
if (deleted != 0 && deleted != 1) {
throw new IllegalStateException("Deleted unexpected number of page metadata records: " + deleted);
}
return;
}
// If we could insert page metadata, we're done.
if (db.insertPageMetadata(contentProviderClient, uri, hasImage, preparedMetadataJSON)) {
return;
}
// Otherwise, we need to queue it for future insertion when history record is available.
Bundle bundledMetadata = new Bundle();
bundledMetadata.putBoolean(KEY_HAS_IMAGE, hasImage);
bundledMetadata.putString(KEY_METADATA_JSON, preparedMetadataJSON);
queuedMetadata.put(uri, bundledMetadata);
}
@VisibleForTesting
/* package-local */ int getMetadataQueueSize() {
return queuedMetadata.size();
}
@Override
public void handleMessage(String event, Bundle message, EventCallback callback) {
ThreadUtils.assertOnBackgroundThread();
if (!GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY.equals(event)) {
return;
}
final String uri = message.getString(GlobalHistory.EVENT_PARAM_URI);
if (TextUtils.isEmpty(uri)) {
return;
}
final Bundle bundledMetadata;
synchronized (queuedMetadata) {
if (!queuedMetadata.containsKey(uri)) {
return;
}
bundledMetadata = queuedMetadata.get(uri);
queuedMetadata.remove(uri);
}
insertMetadataBundleForUri(uri, bundledMetadata);
}
private void insertMetadataBundleForUri(String uri, Bundle bundledMetadata) {
final boolean hasImage = bundledMetadata.getBoolean(KEY_HAS_IMAGE);
final String metadataJSON = bundledMetadata.getString(KEY_METADATA_JSON);
// Acquire CPC, must be released in this function.
final ContentProviderClient contentProviderClient = GeckoAppShell.getApplicationContext()
.getContentResolver()
.acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);
// Pre-conditions...
if (contentProviderClient == null) {
Log.e(LOG_TAG, "Couldn't acquire content provider client");
return;
}
if (TextUtils.isEmpty(metadataJSON)) {
Log.e(LOG_TAG, "Metadata bundle contained empty metadata json");
return;
}
// Insert!
try {
add(
BrowserDB.from(GeckoThread.getActiveProfile()),
contentProviderClient,
uri, hasImage, metadataJSON
);
} finally {
contentProviderClient.release();
}
}
private String prepareJSON(String json) throws JSONException {
return (new JSONObject(json)).toString();
}
}

View file

@ -0,0 +1,51 @@
/* -*- 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;
import android.app.KeyguardManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.support.v4.app.NotificationCompat;
import android.view.Window;
import android.view.WindowManager;
// Utility methods for entering/exiting guest mode.
public final class GuestSession {
private static final String LOGTAG = "GeckoGuestSession";
public static final String NOTIFICATION_INTENT = "org.mozilla.gecko.GUEST_SESSION_INPROGRESS";
private static PendingIntent getNotificationIntent(Context context) {
Intent intent = new Intent(NOTIFICATION_INTENT);
intent.setClassName(context, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
public static void showNotification(Context context) {
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
final Resources res = context.getResources();
builder.setContentTitle(res.getString(R.string.guest_browsing_notification_title))
.setContentText(res.getString(R.string.guest_browsing_notification_text))
.setSmallIcon(R.drawable.alert_guest)
.setOngoing(true)
.setContentIntent(getNotificationIntent(context));
final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(R.id.guestNotification, builder.build());
}
public static void hideNotification(Context context) {
final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(R.id.guestNotification);
}
public static void onNotificationIntentReceived(BrowserApp context) {
context.showGuestModeDialog(BrowserApp.GuestModeDialog.LEAVING);
}
}

View file

@ -0,0 +1,599 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.JSONUtils;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.widget.ExternalIntentDuringPrivateBrowsingPromptFragment;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.provider.Browser;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public final class IntentHelper implements GeckoEventListener,
NativeEventListener {
private static final String LOGTAG = "GeckoIntentHelper";
private static final String[] EVENTS = {
"Intent:GetHandlers",
"Intent:Open",
"Intent:OpenForResult",
};
private static final String[] NATIVE_EVENTS = {
"Intent:OpenNoHandler",
};
// via http://developer.android.com/distribute/tools/promote/linking.html
private static String MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id=";
private static String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url";
/** A partial URI to an error page - the encoded error URI should be appended before loading. */
private static String UNKNOWN_PROTOCOL_URI_PREFIX = "about:neterror?e=unknownProtocolFound&u=";
private static IntentHelper instance;
private final FragmentActivity activity;
private IntentHelper(final FragmentActivity activity) {
this.activity = activity;
EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this, EVENTS);
EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener) this, NATIVE_EVENTS);
}
public static IntentHelper init(final FragmentActivity activity) {
if (instance == null) {
instance = new IntentHelper(activity);
} else {
Log.w(LOGTAG, "IntentHelper.init() called twice, ignoring.");
}
return instance;
}
public static void destroy() {
if (instance != null) {
EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) instance, EVENTS);
EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) instance, NATIVE_EVENTS);
instance = null;
}
}
/**
* Given the inputs to <code>getOpenURIIntent</code>, plus an optional
* package name and class name, create and fire an intent to open the
* provided URI. If a class name is specified but a package name is not,
* we will default to using the current fennec package.
*
* @param targetURI the string spec of the URI to open.
* @param mimeType an optional MIME type string.
* @param packageName an optional app package name.
* @param className an optional intent class name.
* @param action an Android action specifier, such as
* <code>Intent.ACTION_SEND</code>.
* @param title the title to use in <code>ACTION_SEND</code> intents.
* @param showPromptInPrivateBrowsing whether or not the user should be prompted when opening
* this uri from private browsing. This should be true
* when the user doesn't explicitly choose to open an an
* external app (e.g. just clicked a link).
* @return true if the activity started successfully or the user was prompted to open the
* application; false otherwise.
*/
public static boolean openUriExternal(String targetURI,
String mimeType,
String packageName,
String className,
String action,
String title,
final boolean showPromptInPrivateBrowsing) {
final GeckoAppShell.GeckoInterface gi = GeckoAppShell.getGeckoInterface();
final Context activityContext = gi != null ? gi.getActivity() : null;
final Context context = activityContext != null ? activityContext : GeckoAppShell.getApplicationContext();
final Intent intent = getOpenURIIntent(context, targetURI,
mimeType, action, title);
if (intent == null) {
return false;
}
if (!TextUtils.isEmpty(className)) {
if (!TextUtils.isEmpty(packageName)) {
intent.setClassName(packageName, className);
} else {
// Default to using the fennec app context.
intent.setClassName(context, className);
}
}
if (!showPromptInPrivateBrowsing || activityContext == null) {
if (activityContext == null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
} else {
// Ideally we retrieve the Activity from the calling args, rather than
// statically, but since this method is called from Gecko and I'm
// unfamiliar with that code, this is a simpler solution.
final FragmentActivity fragmentActivity = (FragmentActivity) activityContext;
return ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
context, fragmentActivity.getSupportFragmentManager(), intent);
}
}
public static boolean hasHandlersForIntent(Intent intent) {
try {
return !GeckoAppShell.queryIntentActivities(intent).isEmpty();
} catch (Exception ex) {
Log.e(LOGTAG, "Exception in hasHandlersForIntent");
return false;
}
}
public static String[] getHandlersForIntent(Intent intent) {
final PackageManager pm = GeckoAppShell.getApplicationContext().getPackageManager();
try {
final List<ResolveInfo> list = GeckoAppShell.queryIntentActivities(intent);
int numAttr = 4;
final String[] ret = new String[list.size() * numAttr];
for (int i = 0; i < list.size(); i++) {
ResolveInfo resolveInfo = list.get(i);
ret[i * numAttr] = resolveInfo.loadLabel(pm).toString();
if (resolveInfo.isDefault)
ret[i * numAttr + 1] = "default";
else
ret[i * numAttr + 1] = "";
ret[i * numAttr + 2] = resolveInfo.activityInfo.applicationInfo.packageName;
ret[i * numAttr + 3] = resolveInfo.activityInfo.name;
}
return ret;
} catch (Exception ex) {
Log.e(LOGTAG, "Exception in getHandlersForIntent");
return new String[0];
}
}
public static Intent getIntentForActionString(String aAction) {
// Default to the view action if no other action as been specified.
if (TextUtils.isEmpty(aAction)) {
return new Intent(Intent.ACTION_VIEW);
}
return new Intent(aAction);
}
/**
* Given a URI, a MIME type, and a title,
* produce a share intent which can be used to query all activities
* than can open the specified URI.
*
* @param context a <code>Context</code> instance.
* @param targetURI the string spec of the URI to open.
* @param mimeType an optional MIME type string.
* @param title the title to use in <code>ACTION_SEND</code> intents.
* @return an <code>Intent</code>, or <code>null</code> if none could be
* produced.
*/
public static Intent getShareIntent(final Context context,
final String targetURI,
final String mimeType,
final String title) {
Intent shareIntent = getIntentForActionString(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, targetURI);
shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);
shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true);
// Note that EXTRA_TITLE is intended to be used for share dialog
// titles. Common usage (e.g., Pocket) suggests that it's sometimes
// interpreted as an alternate to EXTRA_SUBJECT, so we include it.
shareIntent.putExtra(Intent.EXTRA_TITLE, title);
if (mimeType != null && mimeType.length() > 0) {
shareIntent.setType(mimeType);
}
return shareIntent;
}
/**
* Given a URI, a MIME type, an Android intent "action", and a title,
* produce an intent which can be used to start an activity to open
* the specified URI.
*
* @param context a <code>Context</code> instance.
* @param targetURI the string spec of the URI to open.
* @param mimeType an optional MIME type string.
* @param action an Android action specifier, such as
* <code>Intent.ACTION_SEND</code>.
* @param title the title to use in <code>ACTION_SEND</code> intents.
* @return an <code>Intent</code>, or <code>null</code> if none could be
* produced.
*/
static Intent getOpenURIIntent(final Context context,
final String targetURI,
final String mimeType,
final String action,
final String title) {
// The resultant chooser can return non-exported activities in 4.1 and earlier.
// https://code.google.com/p/android/issues/detail?id=29535
final Intent intent = getOpenURIIntentInner(context, targetURI, mimeType, action, title);
if (intent != null) {
// Some applications use this field to return to the same browser after processing the
// Intent. While there is some danger (e.g. denial of service), other major browsers already
// use it and so it's the norm.
intent.putExtra(Browser.EXTRA_APPLICATION_ID, AppConstants.ANDROID_PACKAGE_NAME);
}
return intent;
}
private static Intent getOpenURIIntentInner(final Context context, final String targetURI,
final String mimeType, final String action, final String title) {
if (action.equalsIgnoreCase(Intent.ACTION_SEND)) {
Intent shareIntent = getShareIntent(context, targetURI, mimeType, title);
return Intent.createChooser(shareIntent,
context.getResources().getString(R.string.share_title));
}
Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
if (!TextUtils.isEmpty(mimeType)) {
Intent intent = getIntentForActionString(action);
intent.setDataAndType(uri, mimeType);
return intent;
}
if (!GeckoAppShell.isUriSafeForScheme(uri)) {
return null;
}
final String scheme = uri.getScheme();
if ("intent".equals(scheme) || "android-app".equals(scheme)) {
final Intent intent;
try {
intent = Intent.parseUri(targetURI, 0);
} catch (final URISyntaxException e) {
Log.e(LOGTAG, "Unable to parse URI - " + e);
return null;
}
final Uri data = intent.getData();
if (data != null && "file".equals(data.normalizeScheme().getScheme())) {
Log.w(LOGTAG, "Blocked intent with \"file://\" data scheme.");
return null;
}
// Only open applications which can accept arbitrary data from a browser.
intent.addCategory(Intent.CATEGORY_BROWSABLE);
// Prevent site from explicitly opening our internal activities, which can leak data.
intent.setComponent(null);
nullIntentSelector(intent);
return intent;
}
// Compute our most likely intent, then check to see if there are any
// custom handlers that would apply.
// Start with the original URI. If we end up modifying it, we'll
// overwrite it.
final String extension = MimeTypeMap.getFileExtensionFromUrl(targetURI);
final Intent intent = getIntentForActionString(action);
intent.setData(uri);
if ("file".equals(scheme)) {
// Only set explicit mimeTypes on file://.
final String mimeType2 = GeckoAppShell.getMimeTypeFromExtension(extension);
intent.setType(mimeType2);
return intent;
}
// Have a special handling for SMS based schemes, as the query parameters
// are not extracted from the URI automatically.
if (!"sms".equals(scheme) && !"smsto".equals(scheme) && !"mms".equals(scheme) && !"mmsto".equals(scheme)) {
return intent;
}
final String query = uri.getEncodedQuery();
if (TextUtils.isEmpty(query)) {
return intent;
}
// It is common to see sms*/mms* uris on the web without '//', it is W3C standard not to have the slashes,
// but android's Uri builder & Uri require the slashes and will interpret those without as malformed.
String currentUri = uri.toString();
String correctlyFormattedDataURIScheme = scheme + "://";
if (!currentUri.contains(correctlyFormattedDataURIScheme)) {
uri = Uri.parse(currentUri.replaceFirst(scheme + ":", correctlyFormattedDataURIScheme));
}
final String[] fields = query.split("&");
boolean shouldUpdateIntent = false;
String resultQuery = "";
for (String field : fields) {
if (field.startsWith("body=")) {
final String body = Uri.decode(field.substring(5));
intent.putExtra("sms_body", body);
shouldUpdateIntent = true;
} else if (field.startsWith("subject=")) {
final String subject = Uri.decode(field.substring(8));
intent.putExtra("subject", subject);
shouldUpdateIntent = true;
} else if (field.startsWith("cc=")) {
final String ccNumber = Uri.decode(field.substring(3));
String phoneNumber = uri.getAuthority();
if (phoneNumber != null) {
uri = uri.buildUpon().encodedAuthority(phoneNumber + ";" + ccNumber).build();
}
shouldUpdateIntent = true;
} else {
resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
}
}
if (!shouldUpdateIntent) {
// No need to rewrite the URI, then.
return intent;
}
// Form a new URI without the extracted fields in the query part, and
// push that into the new Intent.
final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : "";
final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build();
intent.setData(pruned);
return intent;
}
// We create a separate method to better encapsulate the @TargetApi use.
@TargetApi(15)
private static void nullIntentSelector(final Intent intent) {
intent.setSelector(null);
}
/**
* Return a <code>Uri</code> instance which is equivalent to <code>u</code>,
* but with a guaranteed-lowercase scheme as if the API level 16 method
* <code>u.normalizeScheme</code> had been called.
*
* @param u the <code>Uri</code> to normalize.
* @return a <code>Uri</code>, which might be <code>u</code>.
*/
private static Uri normalizeUriScheme(final Uri u) {
final String scheme = u.getScheme();
final String lower = scheme.toLowerCase(Locale.US);
if (lower.equals(scheme)) {
return u;
}
// Otherwise, return a new URI with a normalized scheme.
return u.buildUpon().scheme(lower).build();
}
@Override
public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
if (event.equals("Intent:OpenNoHandler")) {
openNoHandler(message, callback);
}
}
@Override
public void handleMessage(String event, JSONObject message) {
try {
if (event.equals("Intent:GetHandlers")) {
getHandlers(message);
} else if (event.equals("Intent:Open")) {
open(message);
} else if (event.equals("Intent:OpenForResult")) {
openForResult(message);
}
} catch (JSONException e) {
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
}
}
private void getHandlers(JSONObject message) throws JSONException {
final Intent intent = getOpenURIIntent(activity,
message.optString("url"),
message.optString("mime"),
message.optString("action"),
message.optString("title"));
final List<String> appList = Arrays.asList(getHandlersForIntent(intent));
final JSONObject response = new JSONObject();
response.put("apps", new JSONArray(appList));
EventDispatcher.sendResponse(message, response);
}
private void open(JSONObject message) throws JSONException {
openUriExternal(message.optString("url"),
message.optString("mime"),
message.optString("packageName"),
message.optString("className"),
message.optString("action"),
message.optString("title"), false);
}
private void openForResult(final JSONObject message) throws JSONException {
Intent intent = getOpenURIIntent(activity,
message.optString("url"),
message.optString("mime"),
message.optString("action"),
message.optString("title"));
intent.setClassName(message.optString("packageName"), message.optString("className"));
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
final ResultHandler handler = new ResultHandler(message);
try {
ActivityHandlerHelper.startIntentForActivity(activity, intent, handler);
} catch (SecurityException e) {
Log.w(LOGTAG, "Forbidden to launch activity.", e);
}
}
/**
* Opens a URI without any valid handlers on device. In the best case, a package is specified
* and we can bring the user directly to the application page in an app market. If a package is
* not specified and there is a fallback url in the intent extras, we open that url. If neither
* is present, we alert the user that we were unable to open the link.
*
* @param msg A message with the uri with no handlers as the value for the "uri" key
* @param callback A callback that will be called with success & no params if Java loads a page, or with error and
* the uri to load if Java does not load a page
*/
private void openNoHandler(final NativeJSObject msg, final EventCallback callback) {
final String uri = msg.getString("uri");
if (TextUtils.isEmpty(uri)) {
Log.w(LOGTAG, "Received empty URL - loading about:neterror");
callback.sendError(getUnknownProtocolErrorPageUri(""));
return;
}
final Intent intent;
try {
// TODO (bug 1173626): This will not handle android-app uris on non 5.1 devices.
intent = Intent.parseUri(uri, 0);
} catch (final URISyntaxException e) {
String errorUri;
try {
errorUri = getUnknownProtocolErrorPageUri(URLEncoder.encode(uri, "UTF-8"));
} catch (final UnsupportedEncodingException encodingE) {
errorUri = getUnknownProtocolErrorPageUri("");
}
// Don't log the exception to prevent leaking URIs.
Log.w(LOGTAG, "Unable to parse Intent URI - loading about:neterror");
callback.sendError(errorUri);
return;
}
// For this flow, we follow Chrome's lead:
// https://developer.chrome.com/multidevice/android/intents
final String fallbackUrl = intent.getStringExtra(EXTRA_BROWSER_FALLBACK_URL);
if (isFallbackUrlValid(fallbackUrl)) {
// Opens the page in JS.
callback.sendError(fallbackUrl);
} else if (intent.getPackage() != null) {
// Note on alternative flows: we could get the intent package from a component, however, for
// security reasons, components are ignored when opening URIs (bug 1168998) so we should
// ignore it here too.
//
// Our old flow used to prompt the user to search for their app in the market by scheme and
// while this could help the user find a new app, there is not always a correlation in
// scheme to application name and we could end up steering the user wrong (potentially to
// malicious software). Better to leave that one alone.
final String marketUri = MARKET_INTENT_URI_PACKAGE_PREFIX + intent.getPackage();
final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(marketUri));
marketIntent.addCategory(Intent.CATEGORY_BROWSABLE);
marketIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// (Bug 1192436) We don't know if marketIntent matches any Activities (e.g. non-Play
// Store devices). If it doesn't, clicking the link will cause no action to occur.
ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
activity, activity.getSupportFragmentManager(), marketIntent);
callback.sendSuccess(null);
} else {
// We return the error page here, but it will only be shown if we think the load did
// not come from clicking a link. Chrome does not show error pages in that case, and
// many websites have catered to this behavior. For example, the site might set a timeout and load a play
// store url for their app if the intent link fails to load, i.e. the app is not installed.
// These work-arounds would often end with our users seeing about:neterror instead of the intended experience.
// While I feel showing about:neterror is a better solution for users (when not hacked around),
// we should match the status quo for the good of our users.
//
// Don't log the URI to prevent leaking it.
Log.w(LOGTAG, "Unable to open URI, maybe showing neterror");
callback.sendError(getUnknownProtocolErrorPageUri(intent.getData().toString()));
}
}
private static boolean isFallbackUrlValid(@Nullable final String fallbackUrl) {
if (fallbackUrl == null) {
return false;
}
try {
final String anyCaseScheme = new URI(fallbackUrl).getScheme();
final String scheme = (anyCaseScheme == null) ? null : anyCaseScheme.toLowerCase(Locale.US);
if ("http".equals(scheme) || "https".equals(scheme)) {
return true;
} else {
Log.w(LOGTAG, "Fallback URI uses unsupported scheme: " + scheme + ". Try http or https.");
}
} catch (final URISyntaxException e) {
// Do not include Exception to avoid leaking uris.
Log.w(LOGTAG, "URISyntaxException parsing fallback URI");
}
return false;
}
/**
* Returns an about:neterror uri with the unknownProtocolFound text as a parameter.
* @param encodedUri The encoded uri. While the page does not open correctly without specifying
* a uri parameter, it happily accepts the empty String so this argument may
* be the empty String.
*/
private String getUnknownProtocolErrorPageUri(final String encodedUri) {
return UNKNOWN_PROTOCOL_URI_PREFIX + encodedUri;
}
private static class ResultHandler implements ActivityResultHandler {
private final JSONObject message;
public ResultHandler(JSONObject message) {
this.message = message;
}
@Override
public void onActivityResult(int resultCode, Intent data) {
JSONObject response = new JSONObject();
try {
if (data != null) {
if (data.getExtras() != null) {
response.put("extras", JSONUtils.bundleToJSON(data.getExtras()));
}
if (data.getData() != null) {
response.put("uri", data.getData().toString());
}
}
response.put("resultCode", resultCode);
} catch (JSONException e) {
Log.w(LOGTAG, "Error building JSON response.", e);
}
EventDispatcher.sendResponse(message, response);
}
}
}

View file

@ -0,0 +1,110 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import org.mozilla.gecko.customtabs.CustomTabsActivity;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.mozglue.SafeIntent;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.tabqueue.TabQueueHelper;
import org.mozilla.gecko.tabqueue.TabQueueService;
/**
* Activity that receives incoming Intents and dispatches them to the appropriate activities (e.g. browser, custom tabs, web app).
*/
public class LauncherActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeckoAppShell.ensureCrashHandling();
final SafeIntent safeIntent = new SafeIntent(getIntent());
// If it's not a view intent, it won't be a custom tabs intent either. Just launch!
if (!isViewIntentWithURL(safeIntent)) {
dispatchNormalIntent();
// Is this a custom tabs intent, and are custom tabs enabled?
} else if (AppConstants.MOZ_ANDROID_CUSTOM_TABS && isCustomTabsIntent(safeIntent)
&& isCustomTabsEnabled()) {
dispatchCustomTabsIntent();
// Can we dispatch this VIEW action intent to the tab queue service?
} else if (!safeIntent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false)
&& TabQueueHelper.TAB_QUEUE_ENABLED
&& TabQueueHelper.isTabQueueEnabled(this)) {
dispatchTabQueueIntent();
// Dispatch this VIEW action intent to the browser.
} else {
dispatchNormalIntent();
}
finish();
}
/**
* Launch tab queue service to display overlay.
*/
private void dispatchTabQueueIntent() {
Intent intent = new Intent(getIntent());
intent.setClass(getApplicationContext(), TabQueueService.class);
startService(intent);
}
/**
* Launch the browser activity.
*/
private void dispatchNormalIntent() {
Intent intent = new Intent(getIntent());
intent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
filterFlags(intent);
startActivity(intent);
}
private void dispatchCustomTabsIntent() {
Intent intent = new Intent(getIntent());
intent.setClassName(getApplicationContext(), CustomTabsActivity.class.getName());
filterFlags(intent);
startActivity(intent);
}
private static void filterFlags(Intent intent) {
// Explicitly remove the new task and clear task flags (Our browser activity is a single
// task activity and we never want to start a second task here). See bug 1280112.
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_CLEAR_TASK);
// LauncherActivity is started with the "exclude from recents" flag (set in manifest). We do
// not want to propagate this flag from the launcher activity to the browser.
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
}
private static boolean isViewIntentWithURL(@NonNull final SafeIntent safeIntent) {
return Intent.ACTION_VIEW.equals(safeIntent.getAction())
&& safeIntent.getDataString() != null;
}
private static boolean isCustomTabsIntent(@NonNull final SafeIntent safeIntent) {
return isViewIntentWithURL(safeIntent)
&& safeIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION);
}
private boolean isCustomTabsEnabled() {
return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_CUSTOM_TABS, false);
}
}

View file

@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.util.Locale;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
/**
* Implement this interface to provide Fennec's locale switching functionality.
*
* The LocaleManager is responsible for persisting and applying selected locales,
* and correcting configurations after Android has changed them.
*/
public interface LocaleManager {
void initialize(Context context);
/**
* @return true if locale switching is enabled.
*/
boolean isEnabled();
Locale getCurrentLocale(Context context);
String getAndApplyPersistedLocale(Context context);
void correctLocale(Context context, Resources resources, Configuration newConfig);
void updateConfiguration(Context context, Locale locale);
String setSelectedLocale(Context context, String localeCode);
boolean systemLocaleDidChange();
void resetToSystemLocale(Context context);
/**
* Call this in your onConfigurationChanged handler. This method is expected
* to do the appropriate thing: if the user has selected a locale, it
* corrects the incoming configuration; if not, it signals the new locale to
* use.
*/
Locale onSystemConfigurationChanged(Context context, Resources resources, Configuration configuration, Locale currentActivityLocale);
String getFallbackLocaleTag();
}

View file

@ -0,0 +1,136 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.lang.reflect.Method;
import java.util.Locale;
import org.mozilla.gecko.LocaleManager;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.StrictMode;
import android.support.v4.app.FragmentActivity;
import android.support.v7.app.AppCompatActivity;
/**
* This is a helper class to do typical locale switching operations without
* hitting StrictMode errors or adding boilerplate to common activity
* subclasses.
*
* Either call {@link Locales#initializeLocale(Context)} in your
* <code>onCreate</code> method, or inherit from
* <code>LocaleAwareFragmentActivity</code> or <code>LocaleAwareActivity</code>.
*/
public class Locales {
public static LocaleManager getLocaleManager() {
try {
final Class<?> clazz = Class.forName("org.mozilla.gecko.BrowserLocaleManager");
final Method getInstance = clazz.getMethod("getInstance");
final LocaleManager localeManager = (LocaleManager) getInstance.invoke(null);
return localeManager;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void initializeLocale(Context context) {
final LocaleManager localeManager = getLocaleManager();
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
StrictMode.allowThreadDiskWrites();
try {
localeManager.getAndApplyPersistedLocale(context);
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
public static abstract class LocaleAwareAppCompatActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
Locales.initializeLocale(getApplicationContext());
super.onCreate(savedInstanceState);
}
}
public static abstract class LocaleAwareFragmentActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
Locales.initializeLocale(getApplicationContext());
super.onCreate(savedInstanceState);
}
}
public static abstract class LocaleAwareActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
Locales.initializeLocale(getApplicationContext());
super.onCreate(savedInstanceState);
}
}
/**
* Sometimes we want just the language for a locale, not the entire language
* tag. But Java's .getLanguage method is wrong.
*
* This method is equivalent to the first part of
* {@link Locales#getLanguageTag(Locale)}.
*
* @return a language string, such as "he" for the Hebrew locales.
*/
public static String getLanguage(final Locale locale) {
// Can, but should never be, an empty string.
final String language = locale.getLanguage();
// Modernize certain language codes.
if (language.equals("iw")) {
return "he";
}
if (language.equals("in")) {
return "id";
}
if (language.equals("ji")) {
return "yi";
}
return language;
}
/**
* Gecko uses locale codes like "es-ES", whereas a Java {@link Locale}
* stringifies as "es_ES".
*
* This method approximates the Java 7 method
* <code>Locale#toLanguageTag()</code>.
*
* @return a locale string suitable for passing to Gecko.
*/
public static String getLanguageTag(final Locale locale) {
// If this were Java 7:
// return locale.toLanguageTag();
final String language = getLanguage(locale);
final String country = locale.getCountry(); // Can be an empty string.
if (country.equals("")) {
return language;
}
return language + "-" + country;
}
public static Locale parseLocaleCode(final String localeCode) {
int index;
if ((index = localeCode.indexOf('-')) != -1 ||
(index = localeCode.indexOf('_')) != -1) {
final String langCode = localeCode.substring(0, index);
final String countryCode = localeCode.substring(index + 1);
return new Locale(langCode, countryCode);
}
return new Locale(localeCode);
}
}

View file

@ -0,0 +1,131 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONObject;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class MediaCastingBar extends RelativeLayout implements View.OnClickListener, GeckoEventListener {
private static final String LOGTAG = "GeckoMediaCastingBar";
private TextView mCastingTo;
private ImageButton mMediaPlay;
private ImageButton mMediaPause;
private ImageButton mMediaStop;
private boolean mInflated;
public MediaCastingBar(Context context, AttributeSet attrs) {
super(context, attrs);
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"Casting:Started",
"Casting:Paused",
"Casting:Playing",
"Casting:Stopped");
}
public void inflateContent() {
LayoutInflater inflater = LayoutInflater.from(getContext());
View content = inflater.inflate(R.layout.media_casting, this);
mMediaPlay = (ImageButton) content.findViewById(R.id.media_play);
mMediaPlay.setOnClickListener(this);
mMediaPause = (ImageButton) content.findViewById(R.id.media_pause);
mMediaPause.setOnClickListener(this);
mMediaStop = (ImageButton) content.findViewById(R.id.media_stop);
mMediaStop.setOnClickListener(this);
mCastingTo = (TextView) content.findViewById(R.id.media_sending_to);
// Capture clicks on the rest of the view to prevent them from
// leaking into other views positioned below.
content.setOnClickListener(this);
mInflated = true;
}
public void show() {
if (!mInflated)
inflateContent();
setVisibility(VISIBLE);
}
public void hide() {
setVisibility(GONE);
}
public void onDestroy() {
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"Casting:Started",
"Casting:Paused",
"Casting:Playing",
"Casting:Stopped");
}
// View.OnClickListener implementation
@Override
public void onClick(View v) {
final int viewId = v.getId();
if (viewId == R.id.media_play) {
GeckoAppShell.notifyObservers("Casting:Play", "");
mMediaPlay.setVisibility(GONE);
mMediaPause.setVisibility(VISIBLE);
} else if (viewId == R.id.media_pause) {
GeckoAppShell.notifyObservers("Casting:Pause", "");
mMediaPause.setVisibility(GONE);
mMediaPlay.setVisibility(VISIBLE);
} else if (viewId == R.id.media_stop) {
GeckoAppShell.notifyObservers("Casting:Stop", "");
}
}
// GeckoEventListener implementation
@Override
public void handleMessage(final String event, final JSONObject message) {
final String device = message.optString("device");
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
if (event.equals("Casting:Started")) {
show();
if (!TextUtils.isEmpty(device)) {
mCastingTo.setText(device);
} else {
// Should not happen
mCastingTo.setText("");
Log.d(LOGTAG, "Device name is empty.");
}
mMediaPlay.setVisibility(GONE);
mMediaPause.setVisibility(VISIBLE);
} else if (event.equals("Casting:Paused")) {
mMediaPause.setVisibility(GONE);
mMediaPlay.setVisibility(VISIBLE);
} else if (event.equals("Casting:Playing")) {
mMediaPlay.setVisibility(GONE);
mMediaPause.setVisibility(VISIBLE);
} else if (event.equals("Casting:Stopped")) {
hide();
}
}
});
}
}

View file

@ -0,0 +1,323 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.media.MediaControlIntent;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
import com.google.android.gms.cast.CastMediaControlIntent;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.JNITarget;
import org.mozilla.gecko.annotation.ReflectionTarget;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Manages a list of GeckoMediaPlayers methods (i.e. Chromecast/Miracast). Routes messages
* from Gecko to the correct caster based on the id of the display
*/
public class MediaPlayerManager extends Fragment implements NativeEventListener {
/**
* Create a new instance of DetailsFragment, initialized to
* show the text at 'index'.
*/
private static MediaPlayerManager instance = null;
@ReflectionTarget
public static MediaPlayerManager getInstance() {
if (instance != null) {
return instance;
}
if (Versions.feature17Plus) {
instance = (MediaPlayerManager) new PresentationMediaPlayerManager();
} else {
instance = new MediaPlayerManager();
}
return instance;
}
private static final String LOGTAG = "GeckoMediaPlayerManager";
protected boolean isPresentationMode = false; // Used to prevent mirroring when Presentation API is used.
@ReflectionTarget
public static final String MEDIA_PLAYER_TAG = "MPManagerFragment";
private static final boolean SHOW_DEBUG = false;
// Simplified debugging interfaces
private static void debug(String msg, Exception e) {
if (SHOW_DEBUG) {
Log.e(LOGTAG, msg, e);
}
}
private static void debug(String msg) {
if (SHOW_DEBUG) {
Log.d(LOGTAG, msg);
}
}
protected MediaRouter mediaRouter = null;
protected final Map<String, GeckoMediaPlayer> players = new HashMap<String, GeckoMediaPlayer>();
protected final Map<String, GeckoPresentationDisplay> displays = new HashMap<String, GeckoPresentationDisplay>(); // used for Presentation API
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"MediaPlayer:Load",
"MediaPlayer:Start",
"MediaPlayer:Stop",
"MediaPlayer:Play",
"MediaPlayer:Pause",
"MediaPlayer:End",
"MediaPlayer:Mirror",
"MediaPlayer:Message",
"AndroidCastDevice:Start",
"AndroidCastDevice:Stop",
"AndroidCastDevice:SyncDevice");
}
@Override
@JNITarget
public void onDestroy() {
super.onDestroy();
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"MediaPlayer:Load",
"MediaPlayer:Start",
"MediaPlayer:Stop",
"MediaPlayer:Play",
"MediaPlayer:Pause",
"MediaPlayer:End",
"MediaPlayer:Mirror",
"MediaPlayer:Message",
"AndroidCastDevice:Start",
"AndroidCastDevice:Stop",
"AndroidCastDevice:SyncDevice");
}
// GeckoEventListener implementation
@Override
public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) {
debug(event);
if (event.startsWith("MediaPlayer:")) {
final GeckoMediaPlayer player = players.get(message.getString("id"));
if (player == null) {
Log.e(LOGTAG, "Couldn't find a player for this id: " + message.getString("id") + " for message: " + event);
if (callback != null) {
callback.sendError(null);
}
return;
}
if ("MediaPlayer:Play".equals(event)) {
player.play(callback);
} else if ("MediaPlayer:Start".equals(event)) {
player.start(callback);
} else if ("MediaPlayer:Stop".equals(event)) {
player.stop(callback);
} else if ("MediaPlayer:Pause".equals(event)) {
player.pause(callback);
} else if ("MediaPlayer:End".equals(event)) {
player.end(callback);
} else if ("MediaPlayer:Mirror".equals(event)) {
player.mirror(callback);
} else if ("MediaPlayer:Message".equals(event) && message.has("data")) {
player.message(message.getString("data"), callback);
} else if ("MediaPlayer:Load".equals(event)) {
final String url = message.optString("source", "");
final String type = message.optString("type", "video/mp4");
final String title = message.optString("title", "");
player.load(title, url, type, callback);
}
}
if (event.startsWith("AndroidCastDevice:")) {
if ("AndroidCastDevice:Start".equals(event)) {
final GeckoPresentationDisplay display = displays.get(message.getString("id"));
if (display == null) {
Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
return;
}
display.start(callback);
} else if ("AndroidCastDevice:Stop".equals(event)) {
final GeckoPresentationDisplay display = displays.get(message.getString("id"));
if (display == null) {
Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
return;
}
display.stop(callback);
} else if ("AndroidCastDevice:SyncDevice".equals(event)) {
for (Map.Entry<String, GeckoPresentationDisplay> entry : displays.entrySet()) {
GeckoPresentationDisplay display = entry.getValue();
JSONObject json = display.toJSON();
if (json == null) {
break;
}
GeckoAppShell.notifyObservers("AndroidCastDevice:Added", json.toString());
}
}
}
}
private final MediaRouter.Callback callback =
new MediaRouter.Callback() {
@Override
public void onRouteRemoved(MediaRouter router, RouteInfo route) {
debug("onRouteRemoved: route=" + route);
// Remove from media player list.
players.remove(route.getId());
GeckoAppShell.notifyObservers("MediaPlayer:Removed", route.getId());
updatePresentation();
// Remove from presentation display list.
displays.remove(route.getId());
GeckoAppShell.notifyObservers("AndroidCastDevice:Removed", route.getId());
}
@SuppressWarnings("unused")
public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) {
updatePresentation();
}
// These methods aren't used by the support version Media Router
@SuppressWarnings("unused")
public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
updatePresentation();
}
@Override
public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
updatePresentation();
}
@Override
public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
}
@Override
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
debug("onRouteAdded: route=" + route);
final GeckoMediaPlayer player = getMediaPlayerForRoute(route);
saveAndNotifyOfPlayer("MediaPlayer:Added", route, player);
updatePresentation();
final GeckoPresentationDisplay display = getPresentationDisplayForRoute(route);
saveAndNotifyOfDisplay("AndroidCastDevice:Added", route, display);
}
@Override
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
debug("onRouteChanged: route=" + route);
final GeckoMediaPlayer player = players.get(route.getId());
saveAndNotifyOfPlayer("MediaPlayer:Changed", route, player);
updatePresentation();
final GeckoPresentationDisplay display = displays.get(route.getId());
saveAndNotifyOfDisplay("AndroidCastDevice:Changed", route, display);
}
private void saveAndNotifyOfPlayer(final String eventName,
MediaRouter.RouteInfo route,
final GeckoMediaPlayer player) {
if (player == null) {
return;
}
final JSONObject json = player.toJSON();
if (json == null) {
return;
}
players.put(route.getId(), player);
GeckoAppShell.notifyObservers(eventName, json.toString());
}
private void saveAndNotifyOfDisplay(final String eventName,
MediaRouter.RouteInfo route,
final GeckoPresentationDisplay display) {
if (display == null) {
return;
}
final JSONObject json = display.toJSON();
if (json == null) {
return;
}
displays.put(route.getId(), display);
GeckoAppShell.notifyObservers(eventName, json.toString());
}
};
private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) {
try {
if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
return new ChromeCastPlayer(getActivity(), route);
}
} catch (Exception ex) {
debug("Error handling presentation", ex);
}
return null;
}
private GeckoPresentationDisplay getPresentationDisplayForRoute(MediaRouter.RouteInfo route) {
try {
if (route.supportsControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))) {
return new ChromeCastDisplay(getActivity(), route);
}
} catch (Exception ex) {
debug("Error handling presentation", ex);
}
return null;
}
@Override
public void onPause() {
super.onPause();
mediaRouter.removeCallback(callback);
mediaRouter = null;
}
@Override
public void onResume() {
super.onResume();
// The mediaRouter shouldn't exist here, but this is a nice safety check.
if (mediaRouter != null) {
return;
}
mediaRouter = MediaRouter.getInstance(getActivity());
final MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastPlayer.MIRROR_RECEIVER_APP_ID))
.addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))
.build();
mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
}
public void setPresentationMode(boolean isPresentationMode) {
this.isPresentationMode = isPresentationMode;
}
protected void updatePresentation() { /* Overridden in sub-classes. */ }
}

View file

@ -0,0 +1,279 @@
/* -*- 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;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserProvider;
import org.mozilla.gecko.home.ImageLoader;
import org.mozilla.gecko.icons.storage.MemoryStorage;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks2;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
/**
* This is a utility class to keep track of how much memory and disk-space pressure
* the system is under. It receives input from GeckoActivity via the onLowMemory() and
* onTrimMemory() functions, and also listens for some system intents related to
* disk-space notifications. Internally it will track how much memory and disk pressure
* the system is under, and perform various actions to help alleviate the pressure.
*
* Note that since there is no notification for when the system has lots of free memory
* again, this class also assumes that, over time, the system will free up memory. This
* assumption is implemented using a timer that slowly lowers the internal memory
* pressure state if no new low-memory notifications are received.
*
* Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both
* of these classes may be accessed from various threads, and have both been designed to
* be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock
* is allowed to pick up the MemoryMonitor lock, but not vice-versa.
*/
class MemoryMonitor extends BroadcastReceiver {
private static final String LOGTAG = "GeckoMemoryMonitor";
private static final String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP";
private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE";
// Memory pressure levels. Keep these in sync with those in AndroidJavaWrappers.h
private static final int MEMORY_PRESSURE_NONE = 0;
private static final int MEMORY_PRESSURE_CLEANUP = 1;
private static final int MEMORY_PRESSURE_LOW = 2;
private static final int MEMORY_PRESSURE_MEDIUM = 3;
private static final int MEMORY_PRESSURE_HIGH = 4;
private static final MemoryMonitor sInstance = new MemoryMonitor();
static MemoryMonitor getInstance() {
return sInstance;
}
private Context mAppContext;
private final PressureDecrementer mPressureDecrementer;
private int mMemoryPressure; // Synchronized access only.
private volatile boolean mStoragePressure; // Accessed via UI thread intent, background runnables.
private boolean mInited;
private MemoryMonitor() {
mPressureDecrementer = new PressureDecrementer();
mMemoryPressure = MEMORY_PRESSURE_NONE;
}
public void init(final Context context) {
if (mInited) {
return;
}
mAppContext = context.getApplicationContext();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
filter.addAction(ACTION_MEMORY_DUMP);
filter.addAction(ACTION_FORCE_PRESSURE);
mAppContext.registerReceiver(this, filter);
mInited = true;
}
public void onLowMemory() {
Log.d(LOGTAG, "onLowMemory() notification received");
if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) {
// We need to wait on Gecko here, because if we haven't reduced
// memory usage enough when we return from this, Android will kill us.
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
GeckoThread.waitOnGecko();
}
}
}
public void onTrimMemory(int level) {
Log.d(LOGTAG, "onTrimMemory() notification received with level " + level);
if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
// We seem to get this just by entering the task switcher or hitting the home button.
// Seems bogus, because we are the foreground app, or at least not at the end of the LRU list.
// Just ignore it, and if there is a real memory pressure event (CRITICAL, MODERATE, etc),
// we'll respond appropriately.
return;
}
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
// TRIM_MEMORY_MODERATE is the highest level we'll respond to while backgrounded
increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM);
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
increaseMemoryPressure(MEMORY_PRESSURE_LOW);
break;
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP);
break;
default:
Log.d(LOGTAG, "Unhandled onTrimMemory() level " + level);
break;
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) {
Log.d(LOGTAG, "Device storage is low");
mStoragePressure = true;
ThreadUtils.postToBackgroundThread(new StorageReducer(context));
} else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
Log.d(LOGTAG, "Device storage is ok");
mStoragePressure = false;
} else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) {
String label = intent.getStringExtra("label");
if (label == null) {
label = "default";
}
GeckoAppShell.notifyObservers("Memory:Dump", label);
} else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) {
increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
}
}
@WrapForJNI(calledFrom = "ui")
private static native void dispatchMemoryPressure();
private boolean increaseMemoryPressure(int level) {
int oldLevel;
synchronized (this) {
// bump up our level if we're not already higher
if (mMemoryPressure > level) {
return false;
}
oldLevel = mMemoryPressure;
mMemoryPressure = level;
}
Log.d(LOGTAG, "increasing memory pressure to " + level);
// since we don't get notifications for when memory pressure is off,
// we schedule our own timer to slowly back off the memory pressure level.
// note that this will reset the time to next decrement if the decrementer
// is already running, which is the desired behaviour because we just got
// a new low-mem notification.
mPressureDecrementer.start();
if (oldLevel == level) {
// if we're not going to a higher level we probably don't
// need to run another round of the same memory reductions
// we did on the last memory pressure increase.
return false;
}
// TODO hook in memory-reduction stuff for different levels here
if (level >= MEMORY_PRESSURE_MEDIUM) {
//Only send medium or higher events because that's all that is used right now
if (GeckoThread.isRunning()) {
dispatchMemoryPressure();
}
MemoryStorage.get().evictAll();
ImageLoader.clearLruCache();
LocalBroadcastManager.getInstance(mAppContext)
.sendBroadcast(new Intent(BrowserProvider.ACTION_SHRINK_MEMORY));
}
return true;
}
/**
* Thread-safe due to mStoragePressure's volatility.
*/
boolean isUnderStoragePressure() {
return mStoragePressure;
}
private boolean decreaseMemoryPressure() {
int newLevel;
synchronized (this) {
if (mMemoryPressure <= 0) {
return false;
}
newLevel = --mMemoryPressure;
}
Log.d(LOGTAG, "Decreased memory pressure to " + newLevel);
return true;
}
class PressureDecrementer implements Runnable {
private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes
private boolean mPosted;
synchronized void start() {
if (mPosted) {
// cancel the old one before scheduling a new one
ThreadUtils.getBackgroundHandler().removeCallbacks(this);
}
ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
mPosted = true;
}
@Override
public synchronized void run() {
if (!decreaseMemoryPressure()) {
// done decrementing, bail out
mPosted = false;
return;
}
// need to keep decrementing
ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
}
}
private static class StorageReducer implements Runnable {
private final Context mContext;
private final BrowserDB mDB;
public StorageReducer(final Context context) {
this.mContext = context;
// Since this may be called while Fennec is in the background, we don't want to risk accidentally
// using the wrong context. If the profile we get is a guest profile, use the default profile instead.
GeckoProfile profile = GeckoProfile.get(mContext);
if (profile.inGuestMode()) {
// If it was the guest profile, switch to the default one.
profile = GeckoProfile.get(mContext, GeckoProfile.DEFAULT_PROFILE);
}
mDB = BrowserDB.from(profile);
}
@Override
public void run() {
// this might get run right on startup, if so wait 10 seconds and try again
if (!GeckoThread.isRunning()) {
ThreadUtils.getBackgroundHandler().postDelayed(this, 10000);
return;
}
if (!MemoryMonitor.getInstance().isUnderStoragePressure()) {
// Pressure is off, so we can abort.
return;
}
final ContentResolver cr = mContext.getContentResolver();
mDB.expireHistory(cr, BrowserContract.ExpirePriority.AGGRESSIVE);
mDB.removeThumbnails(cr);
// TODO: drop or shrink disk caches
}
}
}

View file

@ -0,0 +1,13 @@
/* -*- 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;
import android.view.MotionEvent;
import android.view.View;
public interface MotionEventInterceptor {
public boolean onInterceptMotionEvent(View view, MotionEvent event);
}

View file

@ -0,0 +1,38 @@
/* -*- 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;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.mozilla.gecko.mozglue.GeckoLoader;
/**
* This broadcast receiver receives ACTION_MY_PACKAGE_REPLACED broadcasts and
* starts procedures that should run after the APK has been updated.
*/
public class PackageReplacedReceiver extends BroadcastReceiver {
public static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || !ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
// This is not the broadcast we are looking for.
return;
}
// Extract Gecko libs to allow them to be loaded from cache on startup.
extractGeckoLibs(context);
}
private static void extractGeckoLibs(final Context context) {
final String resourcePath = context.getPackageResourcePath();
GeckoLoader.loadMozGlue(context);
GeckoLoader.extractGeckoLibs(context, resourcePath);
}
}

View file

@ -0,0 +1,149 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.annotation.TargetApi;
import android.app.Presentation;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.media.MediaRouter;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.view.WindowManager;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.annotation.WrapForJNI;
/**
* A MediaPlayerManager with API 17+ Presentation support.
*/
@TargetApi(17)
public class PresentationMediaPlayerManager extends MediaPlayerManager {
private static final String LOGTAG = "Gecko" + PresentationMediaPlayerManager.class.getSimpleName();
private GeckoPresentation presentation;
public PresentationMediaPlayerManager() {
if (!Versions.feature17Plus) {
throw new IllegalStateException(PresentationMediaPlayerManager.class.getSimpleName() +
" does not support < API 17");
}
}
@Override
public void onStop() {
super.onStop();
if (presentation != null) {
presentation.dismiss();
presentation = null;
}
}
@Override
protected void updatePresentation() {
if (mediaRouter == null) {
return;
}
if (isPresentationMode) {
return;
}
MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
Display display = route != null ? route.getPresentationDisplay() : null;
if (display != null) {
if ((presentation != null) && (presentation.getDisplay() != display)) {
presentation.dismiss();
presentation = null;
}
if (presentation == null) {
final GeckoView geckoView = (GeckoView) getActivity().findViewById(R.id.layer_view);
presentation = new GeckoPresentation(getActivity(), display, geckoView);
try {
presentation.show();
} catch (WindowManager.InvalidDisplayException ex) {
Log.w(LOGTAG, "Couldn't show presentation! Display was removed in "
+ "the meantime.", ex);
presentation = null;
}
}
} else if (presentation != null) {
presentation.dismiss();
presentation = null;
}
}
@WrapForJNI(calledFrom = "ui")
/* protected */ static native void invalidateAndScheduleComposite(GeckoView geckoView);
@WrapForJNI(calledFrom = "ui")
/* protected */ static native void addPresentationSurface(GeckoView geckoView, Surface surface);
@WrapForJNI(calledFrom = "ui")
/* protected */ static native void removePresentationSurface();
private static final class GeckoPresentation extends Presentation {
private SurfaceView mView;
private GeckoView mGeckoView;
public GeckoPresentation(Context context, Display display, GeckoView geckoView) {
super(context, display);
mGeckoView = geckoView;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mView = new SurfaceView(getContext());
setContentView(mView, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mView.getHolder().addCallback(new SurfaceListener(mGeckoView));
}
}
private static final class SurfaceListener implements SurfaceHolder.Callback {
private GeckoView mGeckoView;
public SurfaceListener(GeckoView geckoView) {
mGeckoView = geckoView;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
// Surface changed so force a composite
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
invalidateAndScheduleComposite(mGeckoView);
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
addPresentationSurface(mGeckoView, holder.getSurface());
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
removePresentationSurface();
}
}
}
}

View file

@ -0,0 +1,27 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.GeckoView;
import org.mozilla.gecko.ScreenManagerHelper;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
public class PresentationView extends GeckoView {
private static final String LOGTAG = "PresentationView";
private static final String presentationViewURI = "chrome://browser/content/PresentationView.xul";
public PresentationView(Context context, String deviceId, int screenId) {
super(context);
this.chromeURI = presentationViewURI + "#" + deviceId;
this.screenId = screenId;
}
}

View file

@ -0,0 +1,124 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import android.content.Context;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentAdapter.LayoutResultCallback;
import android.print.PrintDocumentAdapter.WriteResultCallback;
import android.print.PrintDocumentInfo;
import android.print.PrintManager;
import android.print.PageRange;
import android.util.Log;
public class PrintHelper {
private static final String LOGTAG = "GeckoPrintUtils";
public static void printPDF(final Context context) {
GeckoAppShell.sendRequestToGecko(new GeckoRequest("Print:PDF", new JSONObject()) {
@Override
public void onResponse(NativeJSObject nativeJSObject) {
final String filePath = nativeJSObject.getString("file");
final String title = nativeJSObject.getString("title");
finish(context, filePath, title);
}
@Override
public void onError(NativeJSObject error) {
// Gecko didn't respond due to state change, javascript error, etc.
Log.d(LOGTAG, "No response from Gecko on request to generate a PDF");
}
private void finish(final Context context, final String filePath, final String title) {
PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
String jobName = title;
// The adapter methods are all called on the UI thread by the PrintManager. Put the heavyweight code
// in onWrite on the background thread.
PrintDocumentAdapter pda = new PrintDocumentAdapter() {
@Override
public void onWrite(final PageRange[] pages, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal, final WriteResultCallback callback) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
InputStream input = null;
OutputStream output = null;
try {
File pdfFile = new File(filePath);
input = new FileInputStream(pdfFile);
output = new FileOutputStream(destination.getFileDescriptor());
byte[] buf = new byte[8192];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
}
callback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES });
} catch (FileNotFoundException ee) {
Log.d(LOGTAG, "Unable to find the temporary PDF file.");
} catch (IOException ioe) {
Log.e(LOGTAG, "IOException while transferring temporary PDF file: ", ioe);
} finally {
IOUtils.safeStreamClose(input);
IOUtils.safeStreamClose(output);
}
}
});
}
@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {
if (cancellationSignal.isCanceled()) {
callback.onLayoutCancelled();
return;
}
PrintDocumentInfo pdi = new PrintDocumentInfo.Builder(filePath).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build();
callback.onLayoutFinished(pdi, true);
}
@Override
public void onFinish() {
// Remove the temporary file when the printing system is finished.
try {
File pdfFile = new File(filePath);
pdfFile.delete();
} catch (NullPointerException npe) {
// Silence the exception. We only want to delete a real file. We don't
// care if the file doesn't exist.
}
}
};
printManager.print(jobName, pda, null);
}
});
}
}

View file

@ -0,0 +1,28 @@
/* -*- 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;
import android.content.Context;
import org.json.JSONObject;
import org.mozilla.gecko.db.BrowserDB;
public class PrivateTab extends Tab {
public PrivateTab(Context context, int id, String url, boolean external, int parentId, String title) {
super(context, id, url, external, parentId, title);
}
@Override
protected void saveThumbnailToDB(final BrowserDB db) {}
@Override
public void setMetadata(JSONObject metadata) {}
@Override
public boolean isPrivate() {
return true;
}
}

View file

@ -0,0 +1,133 @@
/* -*- 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;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.db.RemoteClient;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.util.SparseBooleanArray;
/**
* A dialog fragment that displays a list of remote clients.
* <p>
* The dialog allows both single (one tap) and multiple (checkbox) selection.
* The dialog's results are communicated via the {@link RemoteClientsListener}
* interface. Either the dialog fragment's <i>target fragment</i> (see
* {@link Fragment#setTargetFragment(Fragment, int)}), or the containing
* <i>activity</i>, must implement that interface. See
* {@link #notifyListener(List)} for details.
*/
public class RemoteClientsDialogFragment extends DialogFragment {
private static final String KEY_TITLE = "title";
private static final String KEY_CHOICE_MODE = "choice_mode";
private static final String KEY_POSITIVE_BUTTON_TEXT = "positive_button_text";
private static final String KEY_CLIENTS = "clients";
public interface RemoteClientsListener {
// Always called on the main UI thread.
public void onClients(List<RemoteClient> clients);
}
public enum ChoiceMode {
SINGLE,
MULTIPLE,
}
public static RemoteClientsDialogFragment newInstance(String title, String positiveButtonText, ChoiceMode choiceMode, ArrayList<RemoteClient> clients) {
final RemoteClientsDialogFragment dialog = new RemoteClientsDialogFragment();
final Bundle args = new Bundle();
args.putString(KEY_TITLE, title);
args.putString(KEY_POSITIVE_BUTTON_TEXT, positiveButtonText);
args.putInt(KEY_CHOICE_MODE, choiceMode.ordinal());
args.putParcelableArrayList(KEY_CLIENTS, clients);
dialog.setArguments(args);
return dialog;
}
public RemoteClientsDialogFragment() {
// Empty constructor is required for DialogFragment.
}
@Override
public void onDestroy() {
super.onDestroy();
GeckoApplication.watchReference(getActivity(), this);
}
protected void notifyListener(List<RemoteClient> clients) {
RemoteClientsListener listener;
try {
listener = (RemoteClientsListener) getTargetFragment();
} catch (ClassCastException e) {
try {
listener = (RemoteClientsListener) getActivity();
} catch (ClassCastException f) {
throw new ClassCastException(getTargetFragment() + " or " + getActivity()
+ " must implement RemoteClientsListener");
}
}
listener.onClients(clients);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final String title = getArguments().getString(KEY_TITLE);
final String positiveButtonText = getArguments().getString(KEY_POSITIVE_BUTTON_TEXT);
final ChoiceMode choiceMode = ChoiceMode.values()[getArguments().getInt(KEY_CHOICE_MODE)];
final ArrayList<RemoteClient> clients = getArguments().getParcelableArrayList(KEY_CLIENTS);
final Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(title);
final String[] clientNames = new String[clients.size()];
for (int i = 0; i < clients.size(); i++) {
clientNames[i] = clients.get(i).name;
}
if (choiceMode == ChoiceMode.MULTIPLE) {
builder.setMultiChoiceItems(clientNames, null, null);
builder.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int which) {
if (which != Dialog.BUTTON_POSITIVE) {
return;
}
final AlertDialog dialog = (AlertDialog) dialogInterface;
final SparseBooleanArray checkedItemPositions = dialog.getListView().getCheckedItemPositions();
final ArrayList<RemoteClient> checked = new ArrayList<RemoteClient>();
for (int i = 0; i < clients.size(); i++) {
if (checkedItemPositions.get(i)) {
checked.add(clients.get(i));
}
}
notifyListener(checked);
}
});
} else {
builder.setItems(clientNames, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int index) {
final ArrayList<RemoteClient> checked = new ArrayList<RemoteClient>();
checked.add(clients.get(index));
notifyListener(checked);
}
});
}
return builder.create();
}
}

View file

@ -0,0 +1,150 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.json.JSONObject;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.PresentationView;
import org.mozilla.gecko.R;
import org.mozilla.gecko.ScreenManagerHelper;
import org.mozilla.gecko.annotation.JNITarget;
import org.mozilla.gecko.annotation.ReflectionTarget;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.gfx.LayerView;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.CastPresentation;
import com.google.android.gms.cast.CastRemoteDisplayLocalService;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.media.MediaControlIntent;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.support.v7.media.MediaRouter;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.widget.RelativeLayout;
import java.util.HashMap;
import java.util.Map;
/*
* Service to keep the remote display running even when the app goes into the background
*/
public class RemotePresentationService extends CastRemoteDisplayLocalService {
private static final String LOGTAG = "RemotePresentationService";
private CastPresentation presentation;
private String deviceId;
private int screenId;
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getDeviceId() {
return deviceId;
}
@Override
public void onCreatePresentation(Display display) {
createPresentation();
}
@Override
public void onDismissPresentation() {
dismissPresentation();
}
private void dismissPresentation() {
if (presentation != null) {
presentation.dismiss();
presentation = null;
ScreenManagerHelper.removeDisplay(screenId);
MediaPlayerManager.getInstance().setPresentationMode(false);
}
}
private void createPresentation() {
dismissPresentation();
MediaPlayerManager.getInstance().setPresentationMode(true);
DisplayMetrics metrics = new DisplayMetrics();
getDisplay().getMetrics(metrics);
screenId = ScreenManagerHelper.addDisplay(ScreenManagerHelper.DISPLAY_VIRTUAL,
metrics.widthPixels,
metrics.heightPixels,
metrics.density);
VirtualPresentation virtualPresentation = new VirtualPresentation(this, getDisplay());
virtualPresentation.setDeviceId(deviceId);
virtualPresentation.setScreenId(screenId);
presentation = (CastPresentation) virtualPresentation;
try {
presentation.show();
} catch (WindowManager.InvalidDisplayException ex) {
Log.e(LOGTAG, "Unable to show presentation, display was removed.", ex);
dismissPresentation();
}
}
}
class VirtualPresentation extends CastPresentation {
private final String LOGTAG = "VirtualPresentation";
private RelativeLayout layout;
private PresentationView view;
private String deviceId;
private int screenId;
public VirtualPresentation(Context context, Display display) {
super(context, display);
}
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public void setScreenId(int screenId) { this.screenId = screenId; }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
* NOTICE: The context get from getContext() is different to the context
* of the application. Presentaion has its own context to get correct
* resources.
*/
// Create new PresentationView
view = new PresentationView(getContext(), deviceId, screenId);
view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
// Create new layout to put the GeckoView
layout = new RelativeLayout(getContext());
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
layout.addView(view);
setContentView(layout);
}
}

View file

@ -0,0 +1,50 @@
/* -*- 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;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.Process;
import android.util.Log;
public class Restarter extends Service {
private static final String LOGTAG = "GeckoRestarter";
private void doRestart(Intent intent) {
final int oldProc = intent.getIntExtra("pid", -1);
if (oldProc < 0) {
return;
}
Process.killProcess(oldProc);
Log.d(LOGTAG, "Killed " + oldProc);
try {
Thread.sleep(100);
} catch (final InterruptedException e) {
}
final Intent restartIntent = (Intent)intent.getParcelableExtra(Intent.EXTRA_INTENT);
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra("didRestart", true)
.setClassName(getApplicationContext(),
AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
startActivity(restartIntent);
Log.d(LOGTAG, "Launched " + restartIntent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
doRestart(intent);
stopSelf(startId);
return Service.START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View file

@ -0,0 +1,43 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.annotation.WrapForJNI;
class ScreenManagerHelper {
/**
* The following display types use the same definition in nsIScreen.idl
*/
final static int DISPLAY_PRIMARY = 0; // primary screen
final static int DISPLAY_EXTERNAL = 1; // wired displays, such as HDMI, DisplayPort, etc.
final static int DISPLAY_VIRTUAL = 2; // wireless displays, such as Chromecast, WiFi-Display, etc.
/**
* Add a new nsScreen when a new display in Android is available.
*
* @param displayType the display type of the nsScreen would be added
* @param width the width of the new nsScreen
* @param height the height of the new nsScreen
* @param density the density of the new nsScreen
*
* @return return the ID of the added nsScreen
*/
@WrapForJNI
public native static int addDisplay(int displayType,
int width,
int height,
float density);
/**
* Remove the nsScreen by the specific screen ID.
*
* @param screenId the ID of the screen would be removed.
*/
@WrapForJNI
public native static void removeDisplay(int screenId);
}

View file

@ -0,0 +1,146 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.permissions.Permissions;
import org.mozilla.gecko.util.ThreadUtils;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
public class ScreenshotObserver {
private static final String LOGTAG = "GeckoScreenshotObserver";
public Context context;
/**
* Listener for screenshot changes.
*/
public interface OnScreenshotListener {
/**
* This callback is executed on the UI thread.
*/
public void onScreenshotTaken(String data, String title);
}
private OnScreenshotListener listener;
public ScreenshotObserver() {
}
public void setListener(Context context, OnScreenshotListener listener) {
this.context = context;
this.listener = listener;
}
private MediaObserver mediaObserver;
private String[] mediaProjections = new String[] {
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.DISPLAY_NAME,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.TITLE
};
/**
* Start ScreenshotObserver if this device is supported and all required runtime permissions
* have been granted by the user. Calling this method will not prompt for permissions.
*/
public void start() {
Permissions.from(context)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.doNotPrompt()
.run(startObserverRunnable());
}
private Runnable startObserverRunnable() {
return new Runnable() {
@Override
public void run() {
try {
if (mediaObserver == null) {
mediaObserver = new MediaObserver();
context.getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mediaObserver);
}
} catch (Exception e) {
Log.e(LOGTAG, "Failure to start watching media: ", e);
}
}
};
}
public void stop() {
if (mediaObserver == null) {
return;
}
try {
context.getContentResolver().unregisterContentObserver(mediaObserver);
mediaObserver = null;
} catch (Exception e) {
Log.e(LOGTAG, "Failure to stop watching media: ", e);
}
}
public void onMediaChange(final Uri uri) {
// Make sure we are on not on the main thread.
final ContentResolver cr = context.getContentResolver();
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// Find the most recent image added to the MediaStore and see if it's a screenshot.
final Cursor cursor = cr.query(uri, mediaProjections, null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " DESC LIMIT 1");
try {
if (cursor == null) {
return;
}
while (cursor.moveToNext()) {
String data = cursor.getString(0);
Log.i(LOGTAG, "data: " + data);
String display = cursor.getString(1);
Log.i(LOGTAG, "display: " + display);
String album = cursor.getString(2);
Log.i(LOGTAG, "album: " + album);
long date = cursor.getLong(3);
String title = cursor.getString(4);
Log.i(LOGTAG, "title: " + title);
if (album != null && album.toLowerCase().contains("screenshot")) {
if (listener != null) {
listener.onScreenshotTaken(data, title);
break;
}
}
}
} catch (Exception e) {
Log.e(LOGTAG, "Failure to process media change: ", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
});
}
private class MediaObserver extends ContentObserver {
public MediaObserver() {
super(null);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
onMediaChange(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
}
}

View file

@ -0,0 +1,140 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* ***** BEGIN LICENSE BLOCK *****
*
* 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/.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko;
import java.util.LinkedList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;
public abstract class SessionParser {
private static final String LOGTAG = "GeckoSessionParser";
public class SessionTab {
final private String mTitle;
final private String mUrl;
final private JSONObject mTabObject;
private boolean mIsSelected;
private SessionTab(String title, String url, boolean isSelected, JSONObject tabObject) {
mTitle = title;
mUrl = url;
mIsSelected = isSelected;
mTabObject = tabObject;
}
public String getTitle() {
return mTitle;
}
public String getUrl() {
return mUrl;
}
public boolean isSelected() {
return mIsSelected;
}
public JSONObject getTabObject() {
return mTabObject;
}
/**
* Is this tab pointing to about:home and does not contain any other history?
*/
public boolean isAboutHomeWithoutHistory() {
JSONArray entries = mTabObject.optJSONArray("entries");
return entries != null && entries.length() == 1 && AboutPages.isAboutHome(mUrl);
}
};
abstract public void onTabRead(SessionTab tab);
/**
* Placeholder method that must be overloaded to handle closedTabs while parsing session data.
*
* @param closedTabs, JSONArray of recently closed tab entries.
* @throws JSONException
*/
public void onClosedTabsRead(final JSONArray closedTabs) throws JSONException {
}
/**
* Parses the provided session store data and calls onTabRead for each tab that has been found.
*
* @param sessionStrings One or more strings containing session store data.
* @return False if any of the session strings provided didn't contain valid session store data.
*/
public boolean parse(String... sessionStrings) {
final LinkedList<SessionTab> sessionTabs = new LinkedList<SessionTab>();
int totalCount = 0;
int selectedIndex = -1;
try {
for (String sessionString : sessionStrings) {
final JSONArray windowsArray = new JSONObject(sessionString).getJSONArray("windows");
if (windowsArray.length() == 0) {
// Session json can be empty if the user has opted out of session restore.
Log.d(LOGTAG, "Session restore file is empty, no session entries found.");
continue;
}
final JSONObject window = windowsArray.getJSONObject(0);
final JSONArray tabs = window.getJSONArray("tabs");
final int optSelected = window.optInt("selected", -1);
final JSONArray closedTabs = window.optJSONArray("closedTabs");
if (closedTabs != null) {
onClosedTabsRead(closedTabs);
}
for (int i = 0; i < tabs.length(); i++) {
final JSONObject tab = tabs.getJSONObject(i);
final int index = tab.getInt("index");
final JSONArray entries = tab.getJSONArray("entries");
if (index < 1 || entries.length() < index) {
Log.w(LOGTAG, "Session entries and index don't agree.");
continue;
}
final JSONObject entry = entries.getJSONObject(index - 1);
final String url = entry.getString("url");
String title = entry.optString("title");
if (title.length() == 0) {
title = url;
}
totalCount++;
boolean selected = false;
if (optSelected == i + 1) {
selected = true;
selectedIndex = totalCount;
}
sessionTabs.add(new SessionTab(title, url, selected, tab));
}
}
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error", e);
return false;
}
// If no selected index was found, select the first tab.
if (selectedIndex == -1 && sessionTabs.size() > 0) {
sessionTabs.getFirst().mIsSelected = true;
}
for (SessionTab tab : sessionTabs) {
onTabRead(tab);
}
return true;
}
}

View file

@ -0,0 +1,311 @@
/* -*- 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;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.util.GeckoEventListener;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import java.util.Map;
import java.util.HashMap;
/**
* Helper class to get, set, and observe Android Shared Preferences.
*/
public final class SharedPreferencesHelper
implements GeckoEventListener
{
public static final String LOGTAG = "GeckoAndSharedPrefs";
// Calculate this once, at initialization. isLoggable is too expensive to
// have in-line in each log call.
private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
private enum Scope {
APP("app"),
PROFILE("profile"),
GLOBAL("global");
public final String key;
private Scope(String key) {
this.key = key;
}
public static Scope forKey(String key) {
for (Scope scope : values()) {
if (scope.key.equals(key)) {
return scope;
}
}
throw new IllegalStateException("SharedPreferences scope must be valid.");
}
}
protected final Context mContext;
// mListeners is not synchronized because it is only updated in
// handleObserve, which is called from Gecko serially.
protected final Map<String, SharedPreferences.OnSharedPreferenceChangeListener> mListeners;
public SharedPreferencesHelper(Context context) {
mContext = context;
mListeners = new HashMap<String, SharedPreferences.OnSharedPreferenceChangeListener>();
EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
if (dispatcher == null) {
Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
return;
}
dispatcher.registerGeckoThreadListener(this,
"SharedPreferences:Set",
"SharedPreferences:Get",
"SharedPreferences:Observe");
}
public synchronized void uninit() {
EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
if (dispatcher == null) {
Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
return;
}
dispatcher.unregisterGeckoThreadListener(this,
"SharedPreferences:Set",
"SharedPreferences:Get",
"SharedPreferences:Observe");
}
private SharedPreferences getSharedPreferences(JSONObject message) throws JSONException {
final Scope scope = Scope.forKey(message.getString("scope"));
switch (scope) {
case APP:
return GeckoSharedPrefs.forApp(mContext);
case PROFILE:
final String profileName = message.optString("profileName", null);
if (profileName == null) {
return GeckoSharedPrefs.forProfile(mContext);
} else {
return GeckoSharedPrefs.forProfileName(mContext, profileName);
}
case GLOBAL:
final String branch = message.optString("branch", null);
if (branch == null) {
return PreferenceManager.getDefaultSharedPreferences(mContext);
} else {
return mContext.getSharedPreferences(branch, Context.MODE_PRIVATE);
}
}
return null;
}
private String getBranch(Scope scope, String profileName, String branch) {
switch (scope) {
case APP:
return GeckoSharedPrefs.APP_PREFS_NAME;
case PROFILE:
if (profileName == null) {
profileName = GeckoProfile.get(mContext).getName();
}
return GeckoSharedPrefs.PROFILE_PREFS_NAME_PREFIX + profileName;
case GLOBAL:
return branch;
}
return null;
}
/**
* Set many SharedPreferences in Android.
*
* message.branch must exist, and should be a String SharedPreferences
* branch name, or null for the default branch.
* message.preferences should be an array of preferences. Each preference
* must include a String name, a String type in ["bool", "int", "string"],
* and an Object value.
*/
private void handleSet(JSONObject message) throws JSONException {
SharedPreferences.Editor editor = getSharedPreferences(message).edit();
JSONArray jsonPrefs = message.getJSONArray("preferences");
for (int i = 0; i < jsonPrefs.length(); i++) {
JSONObject pref = jsonPrefs.getJSONObject(i);
String name = pref.getString("name");
String type = pref.getString("type");
if ("bool".equals(type)) {
editor.putBoolean(name, pref.getBoolean("value"));
} else if ("int".equals(type)) {
editor.putInt(name, pref.getInt("value"));
} else if ("string".equals(type)) {
editor.putString(name, pref.getString("value"));
} else {
Log.w(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]");
}
editor.apply();
}
}
/**
* Get many SharedPreferences from Android.
*
* message.branch must exist, and should be a String SharedPreferences
* branch name, or null for the default branch.
* message.preferences should be an array of preferences. Each preference
* must include a String name, and a String type in ["bool", "int",
* "string"].
*/
private JSONArray handleGet(JSONObject message) throws JSONException {
SharedPreferences prefs = getSharedPreferences(message);
JSONArray jsonPrefs = message.getJSONArray("preferences");
JSONArray jsonValues = new JSONArray();
for (int i = 0; i < jsonPrefs.length(); i++) {
JSONObject pref = jsonPrefs.getJSONObject(i);
String name = pref.getString("name");
String type = pref.getString("type");
JSONObject jsonValue = new JSONObject();
jsonValue.put("name", name);
jsonValue.put("type", type);
try {
if ("bool".equals(type)) {
boolean value = prefs.getBoolean(name, false);
jsonValue.put("value", value);
} else if ("int".equals(type)) {
int value = prefs.getInt(name, 0);
jsonValue.put("value", value);
} else if ("string".equals(type)) {
String value = prefs.getString(name, "");
jsonValue.put("value", value);
} else {
Log.w(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]");
}
} catch (ClassCastException e) {
// Thrown if there is a preference with the given name that is
// not the right type.
Log.w(LOGTAG, "Wrong pref value type [" + type + "] for pref [" + name + "]");
}
jsonValues.put(jsonValue);
}
return jsonValues;
}
private static class ChangeListener
implements SharedPreferences.OnSharedPreferenceChangeListener {
public final Scope scope;
public final String branch;
public final String profileName;
public ChangeListener(final Scope scope, final String branch, final String profileName) {
this.scope = scope;
this.branch = branch;
this.profileName = profileName;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (logVerbose) {
Log.v(LOGTAG, "Got onSharedPreferenceChanged");
}
try {
final JSONObject msg = new JSONObject();
msg.put("scope", this.scope.key);
msg.put("branch", this.branch);
msg.put("profileName", this.profileName);
msg.put("key", key);
// Truly, this is awful, but the API impedance is strong: there
// is no way to get a single untyped value from a
// SharedPreferences instance.
msg.put("value", sharedPreferences.getAll().get(key));
GeckoAppShell.notifyObservers("SharedPreferences:Changed", msg.toString());
} catch (JSONException e) {
Log.e(LOGTAG, "Got exception creating JSON object", e);
return;
}
}
}
/**
* Register or unregister a SharedPreferences.OnSharedPreferenceChangeListener.
*
* message.branch must exist, and should be a String SharedPreferences
* branch name, or null for the default branch.
* message.enable should be a boolean: true to enable listening, false to
* disable listening.
*/
private void handleObserve(JSONObject message) throws JSONException {
final SharedPreferences prefs = getSharedPreferences(message);
final boolean enable = message.getBoolean("enable");
final Scope scope = Scope.forKey(message.getString("scope"));
final String profileName = message.optString("profileName", null);
final String branch = getBranch(scope, profileName, message.optString("branch", null));
if (branch == null) {
Log.e(LOGTAG, "No branch specified for SharedPreference:Observe; aborting.");
return;
}
// mListeners is only modified in this one observer, which is called
// from Gecko serially.
if (enable && !this.mListeners.containsKey(branch)) {
SharedPreferences.OnSharedPreferenceChangeListener listener
= new ChangeListener(scope, branch, profileName);
this.mListeners.put(branch, listener);
prefs.registerOnSharedPreferenceChangeListener(listener);
}
if (!enable && this.mListeners.containsKey(branch)) {
SharedPreferences.OnSharedPreferenceChangeListener listener
= this.mListeners.remove(branch);
prefs.unregisterOnSharedPreferenceChangeListener(listener);
}
}
@Override
public void handleMessage(String event, JSONObject message) {
// Everything here is synchronous and serial, so we need not worry about
// overwriting an in-progress response.
try {
if (event.equals("SharedPreferences:Set")) {
if (logVerbose) {
Log.v(LOGTAG, "Got SharedPreferences:Set message.");
}
handleSet(message);
} else if (event.equals("SharedPreferences:Get")) {
if (logVerbose) {
Log.v(LOGTAG, "Got SharedPreferences:Get message.");
}
JSONObject obj = new JSONObject();
obj.put("values", handleGet(message));
EventDispatcher.sendResponse(message, obj);
} else if (event.equals("SharedPreferences:Observe")) {
if (logVerbose) {
Log.v(LOGTAG, "Got SharedPreferences:Observe message.");
}
handleObserve(message);
} else {
Log.e(LOGTAG, "SharedPreferencesHelper got unexpected message " + event);
return;
}
} catch (JSONException e) {
Log.e(LOGTAG, "Got exception in handleMessage handling event " + event, e);
return;
}
}
}

View file

@ -0,0 +1,249 @@
/* -*- 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;
import org.json.JSONObject;
import android.text.TextUtils;
public class SiteIdentity {
private final String LOGTAG = "GeckoSiteIdentity";
private SecurityMode mSecurityMode;
private boolean mSecure;
private MixedMode mMixedModeActive;
private MixedMode mMixedModeDisplay;
private TrackingMode mTrackingMode;
private String mHost;
private String mOwner;
private String mSupplemental;
private String mCountry;
private String mVerifier;
private String mOrigin;
// The order of the items here relate to image levels in
// site_security_level.xml
public enum SecurityMode {
UNKNOWN("unknown"),
IDENTIFIED("identified"),
VERIFIED("verified"),
CHROMEUI("chromeUI");
private final String mId;
private SecurityMode(String id) {
mId = id;
}
public static SecurityMode fromString(String id) {
if (id == null) {
throw new IllegalArgumentException("Can't convert null String to SiteIdentity");
}
for (SecurityMode mode : SecurityMode.values()) {
if (TextUtils.equals(mode.mId, id)) {
return mode;
}
}
throw new IllegalArgumentException("Could not convert String id to SiteIdentity");
}
@Override
public String toString() {
return mId;
}
}
// The order of the items here relate to image levels in
// site_security_level.xml
public enum MixedMode {
UNKNOWN("unknown"),
MIXED_CONTENT_BLOCKED("blocked"),
MIXED_CONTENT_LOADED("loaded");
private final String mId;
private MixedMode(String id) {
mId = id;
}
public static MixedMode fromString(String id) {
if (id == null) {
throw new IllegalArgumentException("Can't convert null String to MixedMode");
}
for (MixedMode mode : MixedMode.values()) {
if (TextUtils.equals(mode.mId, id.toLowerCase())) {
return mode;
}
}
throw new IllegalArgumentException("Could not convert String id to MixedMode");
}
@Override
public String toString() {
return mId;
}
}
// The order of the items here relate to image levels in
// site_security_level.xml
public enum TrackingMode {
UNKNOWN("unknown"),
TRACKING_CONTENT_BLOCKED("tracking_content_blocked"),
TRACKING_CONTENT_LOADED("tracking_content_loaded");
private final String mId;
private TrackingMode(String id) {
mId = id;
}
public static TrackingMode fromString(String id) {
if (id == null) {
throw new IllegalArgumentException("Can't convert null String to TrackingMode");
}
for (TrackingMode mode : TrackingMode.values()) {
if (TextUtils.equals(mode.mId, id.toLowerCase())) {
return mode;
}
}
throw new IllegalArgumentException("Could not convert String id to TrackingMode");
}
@Override
public String toString() {
return mId;
}
}
public SiteIdentity() {
reset();
}
public void resetIdentity() {
mSecurityMode = SecurityMode.UNKNOWN;
mOrigin = null;
mHost = null;
mOwner = null;
mSupplemental = null;
mCountry = null;
mVerifier = null;
mSecure = false;
}
public void reset() {
resetIdentity();
mMixedModeActive = MixedMode.UNKNOWN;
mMixedModeDisplay = MixedMode.UNKNOWN;
mTrackingMode = TrackingMode.UNKNOWN;
}
void update(JSONObject identityData) {
if (identityData == null) {
reset();
return;
}
try {
JSONObject mode = identityData.getJSONObject("mode");
try {
mMixedModeDisplay = MixedMode.fromString(mode.getString("mixed_display"));
} catch (Exception e) {
mMixedModeDisplay = MixedMode.UNKNOWN;
}
try {
mMixedModeActive = MixedMode.fromString(mode.getString("mixed_active"));
} catch (Exception e) {
mMixedModeActive = MixedMode.UNKNOWN;
}
try {
mTrackingMode = TrackingMode.fromString(mode.getString("tracking"));
} catch (Exception e) {
mTrackingMode = TrackingMode.UNKNOWN;
}
try {
mSecurityMode = SecurityMode.fromString(mode.getString("identity"));
} catch (Exception e) {
resetIdentity();
return;
}
try {
mOrigin = identityData.getString("origin");
mHost = identityData.optString("host", null);
mOwner = identityData.optString("owner", null);
mSupplemental = identityData.optString("supplemental", null);
mCountry = identityData.optString("country", null);
mVerifier = identityData.optString("verifier", null);
mSecure = identityData.optBoolean("secure", false);
} catch (Exception e) {
resetIdentity();
}
} catch (Exception e) {
reset();
}
}
public SecurityMode getSecurityMode() {
return mSecurityMode;
}
public String getOrigin() {
return mOrigin;
}
public String getHost() {
return mHost;
}
public String getOwner() {
return mOwner;
}
public boolean hasOwner() {
return !TextUtils.isEmpty(mOwner);
}
public String getSupplemental() {
return mSupplemental;
}
public String getCountry() {
return mCountry;
}
public boolean hasCountry() {
return !TextUtils.isEmpty(mCountry);
}
public String getVerifier() {
return mVerifier;
}
public boolean isSecure() {
return mSecure;
}
public MixedMode getMixedModeActive() {
return mMixedModeActive;
}
public MixedMode getMixedModeDisplay() {
return mMixedModeDisplay;
}
public TrackingMode getTrackingMode() {
return mTrackingMode;
}
}

View file

@ -0,0 +1,257 @@
/* -*- 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;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeJSObject;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.widget.TextView;
import java.lang.ref.WeakReference;
/**
* Helper class for creating and dismissing snackbars. Use this class to guarantee a consistent style and behavior
* across the app.
*/
public class SnackbarBuilder {
/**
* Combined interface for handling all callbacks from a snackbar because anonymous classes can only extend one
* interface or class.
*/
public static abstract class SnackbarCallback extends Snackbar.Callback implements View.OnClickListener {}
public static final String LOGTAG = "GeckoSnackbarBuilder";
/**
* SnackbarCallback implementation for delegating snackbar events to an EventCallback.
*/
private static class SnackbarEventCallback extends SnackbarCallback {
private EventCallback callback;
public SnackbarEventCallback(EventCallback callback) {
this.callback = callback;
}
@Override
public synchronized void onClick(View view) {
if (callback == null) {
return;
}
callback.sendSuccess(null);
callback = null; // Releasing reference. We only want to execute the callback once.
}
@Override
public synchronized void onDismissed(Snackbar snackbar, int event) {
if (callback == null || event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
return;
}
callback.sendError(null);
callback = null; // Releasing reference. We only want to execute the callback once.
}
}
private static final Object currentSnackbarLock = new Object();
private static WeakReference<Snackbar> currentSnackbar = new WeakReference<>(null); // Guarded by 'currentSnackbarLock'
private final Activity activity;
private String message;
private int duration;
private String action;
private SnackbarCallback callback;
private Drawable icon;
private Integer backgroundColor;
private Integer actionColor;
/**
* @param activity Activity to show the snackbar in.
*/
private SnackbarBuilder(final Activity activity) {
this.activity = activity;
}
public static SnackbarBuilder builder(final Activity activity) {
return new SnackbarBuilder(activity);
}
/**
* @param message The text to show. Can be formatted text.
*/
public SnackbarBuilder message(final String message) {
this.message = message;
return this;
}
/**
* @param id The id of the string resource to show. Can be formatted text.
*/
public SnackbarBuilder message(@StringRes final int id) {
message = activity.getResources().getString(id);
return this;
}
/**
* @param duration How long to display the message.
*/
public SnackbarBuilder duration(final int duration) {
this.duration = duration;
return this;
}
/**
* @param action Action text to display.
*/
public SnackbarBuilder action(final String action) {
this.action = action;
return this;
}
/**
* @param id The id of the string resource for the action text to display.
*/
public SnackbarBuilder action(@StringRes final int id) {
action = activity.getResources().getString(id);
return this;
}
/**
* @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed.
*/
public SnackbarBuilder callback(final SnackbarCallback callback) {
this.callback = callback;
return this;
}
/**
* @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed.
*/
public SnackbarBuilder callback(final EventCallback callback) {
this.callback = new SnackbarEventCallback(callback);
return this;
}
/**
* @param icon Icon to be displayed with the snackbar text.
*/
public SnackbarBuilder icon(final Drawable icon) {
this.icon = icon;
return this;
}
/**
* @param backgroundColor Snackbar background color.
*/
public SnackbarBuilder backgroundColor(final Integer backgroundColor) {
this.backgroundColor = backgroundColor;
return this;
}
/**
* @param actionColor Action text color.
*/
public SnackbarBuilder actionColor(final Integer actionColor) {
this.actionColor = actionColor;
return this;
}
/**
* @param object Populate the builder with data from a Gecko Snackbar:Show event.
*/
public SnackbarBuilder fromEvent(final NativeJSObject object) {
message = object.getString("message");
duration = object.getInt("duration");
if (object.has("backgroundColor")) {
final String providedColor = object.getString("backgroundColor");
try {
backgroundColor = Color.parseColor(providedColor);
} catch (IllegalArgumentException e) {
Log.w(LOGTAG, "Failed to parse color string: " + providedColor);
}
}
NativeJSObject actionObject = object.optObject("action", null);
if (actionObject != null) {
action = actionObject.optString("label", null);
}
return this;
}
public void buildAndShow() {
final View parentView = findBestParentView(activity);
final Snackbar snackbar = Snackbar.make(parentView, message, duration);
if (callback != null && !TextUtils.isEmpty(action)) {
snackbar.setAction(action, callback);
if (actionColor == null) {
snackbar.setActionTextColor(ContextCompat.getColor(activity, R.color.fennec_ui_orange));
} else {
snackbar.setActionTextColor(actionColor);
}
snackbar.setCallback(callback);
}
if (icon != null) {
int leftPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, activity.getResources().getDisplayMetrics());
final InsetDrawable paddedIcon = new InsetDrawable(icon, 0, 0, leftPadding, 0);
paddedIcon.setBounds(0, 0, leftPadding + icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
TextView textView = (TextView) snackbar.getView().findViewById(android.support.design.R.id.snackbar_text);
textView.setCompoundDrawables(paddedIcon, null, null, null);
}
if (backgroundColor != null) {
snackbar.getView().setBackgroundColor(backgroundColor);
}
snackbar.show();
synchronized (currentSnackbarLock) {
currentSnackbar = new WeakReference<>(snackbar);
}
}
/**
* Dismiss the currently visible snackbar.
*/
public static void dismissCurrentSnackbar() {
synchronized (currentSnackbarLock) {
final Snackbar snackbar = currentSnackbar.get();
if (snackbar != null && snackbar.isShown()) {
snackbar.dismiss();
}
}
}
/**
* Find the best parent view to hold the Snackbar's view. The Snackbar implementation of the support
* library will use this view to walk up the view tree to find an actual suitable parent (if needed).
*/
private static View findBestParentView(Activity activity) {
if (activity instanceof GeckoApp) {
final View view = activity.findViewById(R.id.root_layout);
if (view != null) {
return view;
}
}
return activity.findViewById(android.R.id.content);
}
}

View file

@ -0,0 +1,142 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import org.json.JSONArray;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.util.HardwareUtils;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.util.NetworkUtils;
/**
* Use network-based search suggestions.
*/
public class SuggestClient {
private static final String LOGTAG = "GeckoSuggestClient";
// This should go through GeckoInterface to get the UA, but the search activity
// doesn't use a GeckoView yet. Until it does, get the UA directly.
private static final String USER_AGENT = HardwareUtils.isTablet() ?
AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE;
private final Context mContext;
private final int mTimeout;
// should contain the string "__searchTerms__", which is replaced with the query
private final String mSuggestTemplate;
// the maximum number of suggestions to return
private final int mMaxResults;
// used by robocop for testing
private final boolean mCheckNetwork;
// used to make suggestions appear instantly after opt-in
private String mPrevQuery;
private ArrayList<String> mPrevResults;
@RobocopTarget
public SuggestClient(Context context, String suggestTemplate, int timeout, int maxResults, boolean checkNetwork) {
mContext = context;
mMaxResults = maxResults;
mSuggestTemplate = suggestTemplate;
mTimeout = timeout;
mCheckNetwork = checkNetwork;
}
public String getSuggestTemplate() {
return mSuggestTemplate;
}
/**
* Queries for a given search term and returns an ArrayList of suggestions.
*/
public ArrayList<String> query(String query) {
if (query.equals(mPrevQuery))
return mPrevResults;
ArrayList<String> suggestions = new ArrayList<String>();
if (TextUtils.isEmpty(mSuggestTemplate) || TextUtils.isEmpty(query)) {
return suggestions;
}
if (!NetworkUtils.isConnected(mContext) && mCheckNetwork) {
Log.i(LOGTAG, "Not connected to network");
return suggestions;
}
try {
String encoded = URLEncoder.encode(query, "UTF-8");
String suggestUri = mSuggestTemplate.replace("__searchTerms__", encoded);
URL url = new URL(suggestUri);
String json = null;
HttpURLConnection urlConnection = null;
InputStream in = null;
try {
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setConnectTimeout(mTimeout);
urlConnection.setRequestProperty("User-Agent", USER_AGENT);
in = new BufferedInputStream(urlConnection.getInputStream());
json = convertStreamToString(in);
} finally {
if (urlConnection != null)
urlConnection.disconnect();
if (in != null) {
try {
in.close();
} catch (IOException e) {
Log.e(LOGTAG, "error", e);
}
}
}
if (json != null) {
/*
* Sample result:
* ["foo",["food network","foothill college","foot locker",...]]
*/
JSONArray results = new JSONArray(json);
JSONArray jsonSuggestions = results.getJSONArray(1);
int added = 0;
for (int i = 0; (i < jsonSuggestions.length()) && (added < mMaxResults); i++) {
String suggestion = jsonSuggestions.getString(i);
if (!suggestion.equalsIgnoreCase(query)) {
suggestions.add(suggestion);
added++;
}
}
} else {
Log.e(LOGTAG, "Suggestion query failed");
}
} catch (Exception e) {
Log.e(LOGTAG, "Error", e);
}
mPrevQuery = query;
mPrevResults = suggestions;
return suggestions;
}
private String convertStreamToString(java.io.InputStream is) {
try {
return new java.util.Scanner(is).useDelimiter("\\A").next();
} catch (java.util.NoSuchElementException e) {
return "";
}
}
}

View file

@ -0,0 +1,843 @@
/* -*- 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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.URLMetadata;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.icons.IconCallback;
import org.mozilla.gecko.icons.IconDescriptor;
import org.mozilla.gecko.icons.IconRequestBuilder;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.Icons;
import org.mozilla.gecko.reader.ReaderModeUtils;
import org.mozilla.gecko.reader.ReadingListHelper;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.SiteLogins;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
public class Tab {
private static final String LOGTAG = "GeckoTab";
private static Pattern sColorPattern;
private final int mId;
private final BrowserDB mDB;
private long mLastUsed;
private String mUrl;
private String mBaseDomain;
private String mUserRequested; // The original url requested. May be typed by the user or sent by an extneral app for example.
private String mTitle;
private Bitmap mFavicon;
private String mFaviconUrl;
private String mApplicationId; // Intended to be null after explicit user action.
private IconRequestBuilder mIconRequestBuilder;
private Future<IconResponse> mRunningIconRequest;
private boolean mHasFeeds;
private boolean mHasOpenSearch;
private final SiteIdentity mSiteIdentity;
private SiteLogins mSiteLogins;
private BitmapDrawable mThumbnail;
private final int mParentId;
// Indicates the url was loaded from a source external to the app. This will be cleared
// when the user explicitly loads a new url (e.g. clicking a link is not explicit).
private final boolean mExternal;
private boolean mBookmark;
private int mFaviconLoadId;
private String mContentType;
private boolean mHasTouchListeners;
private final ArrayList<View> mPluginViews;
private int mState;
private Bitmap mThumbnailBitmap;
private boolean mDesktopMode;
private boolean mEnteringReaderMode;
private final Context mAppContext;
private ErrorType mErrorType = ErrorType.NONE;
private volatile int mLoadProgress;
private volatile int mRecordingCount;
private volatile boolean mIsAudioPlaying;
private volatile boolean mIsMediaPlaying;
private String mMostRecentHomePanel;
private boolean mShouldShowToolbarWithoutAnimationOnFirstSelection;
/*
* Bundle containing restore data for the panel referenced in mMostRecentHomePanel. This can be
* e.g. the most recent folder for the bookmarks panel, or any other state that should be
* persisted. This is then used e.g. when returning to homepanels via history.
*/
private Bundle mMostRecentHomePanelData;
private int mHistoryIndex;
private int mHistorySize;
private boolean mCanDoBack;
private boolean mCanDoForward;
private boolean mIsEditing;
private final TabEditingState mEditingState = new TabEditingState();
// Will be true when tab is loaded from cache while device was offline.
private boolean mLoadedFromCache;
public static final int STATE_DELAYED = 0;
public static final int STATE_LOADING = 1;
public static final int STATE_SUCCESS = 2;
public static final int STATE_ERROR = 3;
public static final int LOAD_PROGRESS_INIT = 10;
public static final int LOAD_PROGRESS_START = 20;
public static final int LOAD_PROGRESS_LOCATION_CHANGE = 60;
public static final int LOAD_PROGRESS_LOADED = 80;
public static final int LOAD_PROGRESS_STOP = 100;
public enum ErrorType {
CERT_ERROR, // Pages with certificate problems
BLOCKED, // Pages blocked for phishing or malware warnings
NET_ERROR, // All other types of error
NONE // Non error pages
}
public Tab(Context context, int id, String url, boolean external, int parentId, String title) {
mAppContext = context.getApplicationContext();
mDB = BrowserDB.from(context);
mId = id;
mUrl = url;
mBaseDomain = "";
mUserRequested = "";
mExternal = external;
mParentId = parentId;
mTitle = title == null ? "" : title;
mSiteIdentity = new SiteIdentity();
mHistoryIndex = -1;
mContentType = "";
mPluginViews = new ArrayList<View>();
mState = shouldShowProgress(url) ? STATE_LOADING : STATE_SUCCESS;
mLoadProgress = LOAD_PROGRESS_INIT;
mIconRequestBuilder = Icons.with(mAppContext).pageUrl(mUrl);
updateBookmark();
}
private ContentResolver getContentResolver() {
return mAppContext.getContentResolver();
}
public void onDestroy() {
Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.CLOSED);
}
@RobocopTarget
public int getId() {
return mId;
}
public synchronized void onChange() {
mLastUsed = System.currentTimeMillis();
}
public synchronized long getLastUsed() {
return mLastUsed;
}
public int getParentId() {
return mParentId;
}
// may be null if user-entered query hasn't yet been resolved to a URI
public synchronized String getURL() {
return mUrl;
}
// mUserRequested should never be null, but it may be an empty string
public synchronized String getUserRequested() {
return mUserRequested;
}
// mTitle should never be null, but it may be an empty string
public synchronized String getTitle() {
return mTitle;
}
public String getDisplayTitle() {
if (mTitle != null && mTitle.length() > 0) {
return mTitle;
}
return mUrl;
}
/**
* Returns the base domain of the loaded uri. Note that if the page is
* a Reader mode uri, the base domain returned is that of the original uri.
*/
public String getBaseDomain() {
return mBaseDomain;
}
public Bitmap getFavicon() {
return mFavicon;
}
protected String getApplicationId() {
return mApplicationId;
}
protected void setApplicationId(final String applicationId) {
mApplicationId = applicationId;
}
public BitmapDrawable getThumbnail() {
return mThumbnail;
}
public String getMostRecentHomePanel() {
return mMostRecentHomePanel;
}
public Bundle getMostRecentHomePanelData() {
return mMostRecentHomePanelData;
}
public void setMostRecentHomePanel(String panelId) {
mMostRecentHomePanel = panelId;
mMostRecentHomePanelData = null;
}
public void setMostRecentHomePanelData(Bundle data) {
mMostRecentHomePanelData = data;
}
public Bitmap getThumbnailBitmap(int width, int height) {
if (mThumbnailBitmap != null) {
// Bug 787318 - Honeycomb has a bug with bitmap caching, we can't
// reuse the bitmap there.
boolean honeycomb = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2);
boolean sizeChange = mThumbnailBitmap.getWidth() != width
|| mThumbnailBitmap.getHeight() != height;
if (honeycomb || sizeChange) {
mThumbnailBitmap = null;
}
}
if (mThumbnailBitmap == null) {
Bitmap.Config config = (GeckoAppShell.getScreenDepth() == 24) ?
Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
mThumbnailBitmap = Bitmap.createBitmap(width, height, config);
}
return mThumbnailBitmap;
}
public void updateThumbnail(final Bitmap b, final ThumbnailHelper.CachePolicy cachePolicy) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
if (b != null) {
try {
mThumbnail = new BitmapDrawable(mAppContext.getResources(), b);
if (mState == Tab.STATE_SUCCESS && cachePolicy == ThumbnailHelper.CachePolicy.STORE) {
saveThumbnailToDB(mDB);
} else {
// If the page failed to load, or requested that we not cache info about it, clear any previous
// thumbnails we've stored.
clearThumbnailFromDB(mDB);
}
} catch (OutOfMemoryError oom) {
Log.w(LOGTAG, "Unable to create/scale bitmap.", oom);
mThumbnail = null;
}
} else {
mThumbnail = null;
}
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.THUMBNAIL);
}
});
}
public synchronized String getFaviconURL() {
return mFaviconUrl;
}
public boolean hasFeeds() {
return mHasFeeds;
}
public boolean hasOpenSearch() {
return mHasOpenSearch;
}
public boolean hasLoadedFromCache() {
return mLoadedFromCache;
}
public SiteIdentity getSiteIdentity() {
return mSiteIdentity;
}
public void resetSiteIdentity() {
if (mSiteIdentity != null) {
mSiteIdentity.reset();
Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.SECURITY_CHANGE);
}
}
public SiteLogins getSiteLogins() {
return mSiteLogins;
}
public boolean isBookmark() {
return mBookmark;
}
public boolean isExternal() {
return mExternal;
}
public synchronized void updateURL(String url) {
if (url != null && url.length() > 0) {
mUrl = url;
}
}
public synchronized void updateUserRequested(String userRequested) {
mUserRequested = userRequested;
}
public void setErrorType(String type) {
if ("blocked".equals(type))
setErrorType(ErrorType.BLOCKED);
else if ("certerror".equals(type))
setErrorType(ErrorType.CERT_ERROR);
else if ("neterror".equals(type))
setErrorType(ErrorType.NET_ERROR);
else
setErrorType(ErrorType.NONE);
}
public void setErrorType(ErrorType type) {
mErrorType = type;
}
public void setMetadata(JSONObject metadata) {
if (metadata == null) {
return;
}
final ContentResolver cr = mAppContext.getContentResolver();
final URLMetadata urlMetadata = mDB.getURLMetadata();
final Map<String, Object> data = urlMetadata.fromJSON(metadata);
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
urlMetadata.save(cr, data);
}
});
}
public ErrorType getErrorType() {
return mErrorType;
}
public void setContentType(String contentType) {
mContentType = (contentType == null) ? "" : contentType;
}
public String getContentType() {
return mContentType;
}
public int getHistoryIndex() {
return mHistoryIndex;
}
public int getHistorySize() {
return mHistorySize;
}
public synchronized void updateTitle(String title) {
// Keep the title unchanged while entering reader mode.
if (mEnteringReaderMode) {
return;
}
// If there was a title, but it hasn't changed, do nothing.
if (mTitle != null &&
TextUtils.equals(mTitle, title)) {
return;
}
mTitle = (title == null ? "" : title);
Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.TITLE);
}
public void setState(int state) {
mState = state;
if (mState != Tab.STATE_LOADING)
mEnteringReaderMode = false;
}
public int getState() {
return mState;
}
public void setHasTouchListeners(boolean aValue) {
mHasTouchListeners = aValue;
}
public boolean getHasTouchListeners() {
return mHasTouchListeners;
}
public synchronized void addFavicon(String faviconURL, int faviconSize, String mimeType) {
mIconRequestBuilder
.icon(IconDescriptor.createFavicon(faviconURL, faviconSize, mimeType))
.deferBuild();
}
public synchronized void addTouchicon(String iconUrl, int faviconSize, String mimeType) {
mIconRequestBuilder
.icon(IconDescriptor.createTouchicon(iconUrl, faviconSize, mimeType))
.deferBuild();
}
public void loadFavicon() {
// Static Favicons never change
if (AboutPages.isBuiltinIconPage(mUrl) && mFavicon != null) {
return;
}
mRunningIconRequest = mIconRequestBuilder
.build()
.execute(new IconCallback() {
@Override
public void onIconResponse(IconResponse response) {
mFavicon = response.getBitmap();
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.FAVICON);
}
});
}
public synchronized void clearFavicon() {
// Cancel any ongoing favicon load (if we never finished downloading the old favicon before
// we changed page).
if (mRunningIconRequest != null) {
mRunningIconRequest.cancel(true);
}
// Keep the favicon unchanged while entering reader mode
if (mEnteringReaderMode)
return;
mFavicon = null;
mFaviconUrl = null;
}
public void setHasFeeds(boolean hasFeeds) {
mHasFeeds = hasFeeds;
}
public void setHasOpenSearch(boolean hasOpenSearch) {
mHasOpenSearch = hasOpenSearch;
}
public void setLoadedFromCache(boolean loadedFromCache) {
mLoadedFromCache = loadedFromCache;
}
public void updateIdentityData(JSONObject identityData) {
mSiteIdentity.update(identityData);
}
public void setSiteLogins(SiteLogins siteLogins) {
mSiteLogins = siteLogins;
}
void updateBookmark() {
if (getURL() == null) {
return;
}
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final String url = getURL();
if (url == null) {
return;
}
final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(url);
mBookmark = mDB.isBookmark(getContentResolver(), pageUrl);
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.MENU_UPDATED);
}
});
}
public void addBookmark() {
final String url = getURL();
if (url == null) {
return;
}
final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(getURL());
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
mDB.addBookmark(getContentResolver(), mTitle, pageUrl);
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.BOOKMARK_ADDED);
}
});
if (AboutPages.isAboutReader(url)) {
ReadingListHelper.cacheReaderItem(pageUrl, mId, mAppContext);
}
}
public void removeBookmark() {
final String url = getURL();
if (url == null) {
return;
}
final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(getURL());
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
mDB.removeBookmarksWithURL(getContentResolver(), pageUrl);
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.BOOKMARK_REMOVED);
}
});
// We need to ensure we remove readercached items here - we could have switched out of readermode
// before unbookmarking, so we don't necessarily have an about:reader URL here.
ReadingListHelper.removeCachedReaderItem(pageUrl, mAppContext);
}
public boolean isEnteringReaderMode() {
return mEnteringReaderMode;
}
public void doReload(boolean bypassCache) {
GeckoAppShell.notifyObservers("Session:Reload", "{\"bypassCache\":" + String.valueOf(bypassCache) + "}");
}
// Our version of nsSHistory::GetCanGoBack
public boolean canDoBack() {
return mCanDoBack;
}
public boolean doBack() {
if (!canDoBack())
return false;
GeckoAppShell.notifyObservers("Session:Back", "");
return true;
}
public void doStop() {
GeckoAppShell.notifyObservers("Session:Stop", "");
}
// Our version of nsSHistory::GetCanGoForward
public boolean canDoForward() {
return mCanDoForward;
}
public boolean doForward() {
if (!canDoForward())
return false;
GeckoAppShell.notifyObservers("Session:Forward", "");
return true;
}
void handleLocationChange(JSONObject message) throws JSONException {
final String uri = message.getString("uri");
final String oldUrl = getURL();
final boolean sameDocument = message.getBoolean("sameDocument");
mEnteringReaderMode = ReaderModeUtils.isEnteringReaderMode(oldUrl, uri);
mHistoryIndex = message.getInt("historyIndex");
mHistorySize = message.getInt("historySize");
mCanDoBack = message.getBoolean("canGoBack");
mCanDoForward = message.getBoolean("canGoForward");
if (!TextUtils.equals(oldUrl, uri)) {
updateURL(uri);
updateBookmark();
if (!sameDocument) {
// We can unconditionally clear the favicon and title here: we
// already filtered both cases in which this was a (pseudo-)
// spurious location change, so we're definitely loading a new
// page.
clearFavicon();
// Start to build a new request to load a favicon.
mIconRequestBuilder = Icons.with(mAppContext)
.pageUrl(uri);
// Load local static Favicons immediately
if (AboutPages.isBuiltinIconPage(uri)) {
loadFavicon();
}
updateTitle(null);
}
}
if (sameDocument) {
// We can get a location change event for the same document with an anchor tag
// Notify listeners so that buttons like back or forward will update themselves
Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
return;
}
setContentType(message.getString("contentType"));
updateUserRequested(message.getString("userRequested"));
mBaseDomain = message.optString("baseDomain");
setHasFeeds(false);
setHasOpenSearch(false);
mSiteIdentity.reset();
setSiteLogins(null);
setHasTouchListeners(false);
setErrorType(ErrorType.NONE);
setLoadProgressIfLoading(LOAD_PROGRESS_LOCATION_CHANGE);
Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
}
private static boolean shouldShowProgress(final String url) {
return !AboutPages.isAboutPage(url);
}
void handleDocumentStart(boolean restoring, String url) {
setLoadProgress(LOAD_PROGRESS_START);
setState((!restoring && shouldShowProgress(url)) ? STATE_LOADING : STATE_SUCCESS);
mSiteIdentity.reset();
}
void handleDocumentStop(boolean success) {
setState(success ? STATE_SUCCESS : STATE_ERROR);
final String oldURL = getURL();
final Tab tab = this;
tab.setLoadProgress(LOAD_PROGRESS_STOP);
ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
@Override
public void run() {
// tab.getURL() may return null
if (!TextUtils.equals(oldURL, getURL()))
return;
ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab);
}
}, 500);
}
void handleContentLoaded() {
setLoadProgressIfLoading(LOAD_PROGRESS_LOADED);
}
protected void saveThumbnailToDB(final BrowserDB db) {
final BitmapDrawable thumbnail = mThumbnail;
if (thumbnail == null) {
return;
}
try {
final String url = getURL();
if (url == null) {
return;
}
db.updateThumbnailForUrl(getContentResolver(), url, thumbnail);
} catch (Exception e) {
// ignore
}
}
public void loadThumbnailFromDB(final BrowserDB db) {
try {
final String url = getURL();
if (url == null) {
return;
}
byte[] thumbnail = db.getThumbnailForUrl(getContentResolver(), url);
if (thumbnail == null) {
return;
}
Bitmap bitmap = BitmapUtils.decodeByteArray(thumbnail);
mThumbnail = new BitmapDrawable(mAppContext.getResources(), bitmap);
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.THUMBNAIL);
} catch (Exception e) {
// ignore
}
}
private void clearThumbnailFromDB(final BrowserDB db) {
try {
final String url = getURL();
if (url == null) {
return;
}
// Passing in a null thumbnail will delete the stored thumbnail for this url
db.updateThumbnailForUrl(getContentResolver(), url, null);
} catch (Exception e) {
// ignore
}
}
public void addPluginView(View view) {
mPluginViews.add(view);
}
public void removePluginView(View view) {
mPluginViews.remove(view);
}
public View[] getPluginViews() {
return mPluginViews.toArray(new View[mPluginViews.size()]);
}
public void setDesktopMode(boolean enabled) {
mDesktopMode = enabled;
}
public boolean getDesktopMode() {
return mDesktopMode;
}
public boolean isPrivate() {
return false;
}
/**
* Sets the tab load progress to the given percentage.
*
* @param progressPercentage Percentage to set progress to (0-100)
*/
void setLoadProgress(int progressPercentage) {
mLoadProgress = progressPercentage;
}
/**
* Sets the tab load progress to the given percentage only if the tab is
* currently loading.
*
* about:neterror can trigger a STOP before other page load events (bug
* 976426), so any post-START events should make sure the page is loading
* before updating progress.
*
* @param progressPercentage Percentage to set progress to (0-100)
*/
void setLoadProgressIfLoading(int progressPercentage) {
if (getState() == STATE_LOADING) {
setLoadProgress(progressPercentage);
}
}
/**
* Gets the tab load progress percentage.
*
* @return Current progress percentage
*/
public int getLoadProgress() {
return mLoadProgress;
}
public void setRecording(boolean isRecording) {
if (isRecording) {
mRecordingCount++;
} else {
mRecordingCount--;
}
}
public boolean isRecording() {
return mRecordingCount > 0;
}
/**
* The "MediaPlaying" is used for controling media control interface and
* means the tab has playing media.
*
* @param isMediaPlaying the tab has any playing media or not
*/
public void setIsMediaPlaying(boolean isMediaPlaying) {
mIsMediaPlaying = isMediaPlaying;
}
public boolean isMediaPlaying() {
return mIsMediaPlaying;
}
/**
* The "AudioPlaying" is used for showing the tab sound indicator and means
* the tab has playing media and the media is audible.
*
* @param isAudioPlaying the tab has any audible playing media or not
*/
public void setIsAudioPlaying(boolean isAudioPlaying) {
mIsAudioPlaying = isAudioPlaying;
}
public boolean isAudioPlaying() {
return mIsAudioPlaying;
}
public boolean isEditing() {
return mIsEditing;
}
public void setIsEditing(final boolean isEditing) {
this.mIsEditing = isEditing;
}
public TabEditingState getEditingState() {
return mEditingState;
}
public void setShouldShowToolbarWithoutAnimationOnFirstSelection(final boolean shouldShowWithoutAnimation) {
mShouldShowToolbarWithoutAnimationOnFirstSelection = shouldShowWithoutAnimation;
}
public boolean getShouldShowToolbarWithoutAnimationOnFirstSelection() {
return mShouldShowToolbarWithoutAnimationOnFirstSelection;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,246 @@
/* -*- 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;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.TelemetryContract.Event;
import org.mozilla.gecko.TelemetryContract.Method;
import org.mozilla.gecko.TelemetryContract.Reason;
import org.mozilla.gecko.TelemetryContract.Session;
import android.os.SystemClock;
import android.util.Log;
/**
* All telemetry times are relative to one of two clocks:
*
* * Real time since the device was booted, including deep sleep. Use this
* as a substitute for wall clock.
* * Uptime since the device was booted, excluding deep sleep. Use this to
* avoid timing a user activity when their phone is in their pocket!
*
* The majority of methods in this class are defined in terms of real time.
*/
@RobocopTarget
public class Telemetry {
private static final String LOGTAG = "Telemetry";
@WrapForJNI(stubName = "AddHistogram", dispatchTo = "gecko")
private static native void nativeAddHistogram(String name, int value);
@WrapForJNI(stubName = "AddKeyedHistogram", dispatchTo = "gecko")
private static native void nativeAddKeyedHistogram(String name, String key, int value);
@WrapForJNI(stubName = "StartUISession", dispatchTo = "gecko")
private static native void nativeStartUiSession(String name, long timestamp);
@WrapForJNI(stubName = "StopUISession", dispatchTo = "gecko")
private static native void nativeStopUiSession(String name, String reason, long timestamp);
@WrapForJNI(stubName = "AddUIEvent", dispatchTo = "gecko")
private static native void nativeAddUiEvent(String action, String method,
long timestamp, String extras);
public static long uptime() {
return SystemClock.uptimeMillis();
}
public static long realtime() {
return SystemClock.elapsedRealtime();
}
// Define new histograms in:
// toolkit/components/telemetry/Histograms.json
public static void addToHistogram(String name, int value) {
if (GeckoThread.isRunning()) {
nativeAddHistogram(name, value);
} else {
GeckoThread.queueNativeCall(Telemetry.class, "nativeAddHistogram",
String.class, name, value);
}
}
public static void addToKeyedHistogram(String name, String key, int value) {
if (GeckoThread.isRunning()) {
nativeAddKeyedHistogram(name, key, value);
} else {
GeckoThread.queueNativeCall(Telemetry.class, "nativeAddKeyedHistogram",
String.class, name, String.class, key, value);
}
}
public abstract static class Timer {
private final long mStartTime;
private final String mName;
private volatile boolean mHasFinished;
private volatile long mElapsed = -1;
protected abstract long now();
public Timer(String name) {
mName = name;
mStartTime = now();
}
public void cancel() {
mHasFinished = true;
}
public long getElapsed() {
return mElapsed;
}
public void stop() {
// Only the first stop counts.
if (mHasFinished) {
return;
}
mHasFinished = true;
final long elapsed = now() - mStartTime;
if (elapsed < 0) {
Log.e(LOGTAG, "Current time less than start time -- clock shenanigans?");
return;
}
mElapsed = elapsed;
if (elapsed > Integer.MAX_VALUE) {
Log.e(LOGTAG, "Duration of " + elapsed + "ms is too great to add to histogram.");
return;
}
addToHistogram(mName, (int) (elapsed));
}
}
public static class RealtimeTimer extends Timer {
public RealtimeTimer(String name) {
super(name);
}
@Override
protected long now() {
return Telemetry.realtime();
}
}
public static class UptimeTimer extends Timer {
public UptimeTimer(String name) {
super(name);
}
@Override
protected long now() {
return Telemetry.uptime();
}
}
public static void startUISession(final Session session, final String sessionNameSuffix) {
final String sessionName = getSessionName(session, sessionNameSuffix);
Log.d(LOGTAG, "StartUISession: " + sessionName);
if (GeckoThread.isRunning()) {
nativeStartUiSession(sessionName, realtime());
} else {
GeckoThread.queueNativeCall(Telemetry.class, "nativeStartUiSession",
String.class, sessionName, realtime());
}
}
public static void startUISession(final Session session) {
startUISession(session, null);
}
public static void stopUISession(final Session session, final String sessionNameSuffix,
final Reason reason) {
final String sessionName = getSessionName(session, sessionNameSuffix);
Log.d(LOGTAG, "StopUISession: " + sessionName + ", reason=" + reason);
if (GeckoThread.isRunning()) {
nativeStopUiSession(sessionName, reason.toString(), realtime());
} else {
GeckoThread.queueNativeCall(Telemetry.class, "nativeStopUiSession",
String.class, sessionName,
String.class, reason.toString(), realtime());
}
}
public static void stopUISession(final Session session, final Reason reason) {
stopUISession(session, null, reason);
}
public static void stopUISession(final Session session, final String sessionNameSuffix) {
stopUISession(session, sessionNameSuffix, Reason.NONE);
}
public static void stopUISession(final Session session) {
stopUISession(session, null, Reason.NONE);
}
private static String getSessionName(final Session session, final String sessionNameSuffix) {
if (sessionNameSuffix != null) {
return session.toString() + ":" + sessionNameSuffix;
} else {
return session.toString();
}
}
/**
* @param method A non-null method (if null is desired, consider using Method.NONE)
*/
private static void sendUIEvent(final String eventName, final Method method,
final long timestamp, final String extras) {
if (method == null) {
throw new IllegalArgumentException("Expected non-null method - use Method.NONE?");
}
if (!AppConstants.RELEASE_OR_BETA) {
final String logString = "SendUIEvent: event = " + eventName + " method = " + method + " timestamp = " +
timestamp + " extras = " + extras;
Log.d(LOGTAG, logString);
}
if (GeckoThread.isRunning()) {
nativeAddUiEvent(eventName, method.toString(), timestamp, extras);
} else {
GeckoThread.queueNativeCall(Telemetry.class, "nativeAddUiEvent",
String.class, eventName, String.class, method.toString(),
timestamp, String.class, extras);
}
}
public static void sendUIEvent(final Event event, final Method method, final long timestamp,
final String extras) {
sendUIEvent(event.toString(), method, timestamp, extras);
}
public static void sendUIEvent(final Event event, final Method method, final long timestamp) {
sendUIEvent(event, method, timestamp, null);
}
public static void sendUIEvent(final Event event, final Method method, final String extras) {
sendUIEvent(event, method, realtime(), extras);
}
public static void sendUIEvent(final Event event, final Method method) {
sendUIEvent(event, method, realtime(), null);
}
public static void sendUIEvent(final Event event) {
sendUIEvent(event, Method.NONE, realtime(), null);
}
/**
* Sends a UIEvent with the given status appended to the event name.
*
* This method is a slight bend of the Telemetry framework so chances
* are that you don't want to use this: please think really hard before you do.
*
* Intended for use with data policy notifications.
*/
public static void sendUIEvent(final Event event, final boolean eventStatus) {
final String eventName = event + ":" + eventStatus;
sendUIEvent(eventName, Method.NONE, realtime(), null);
}
}

View file

@ -0,0 +1,307 @@
/* -*- 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;
import org.mozilla.gecko.annotation.RobocopTarget;
/**
* Holds data definitions for our UI Telemetry implementation.
*
* Note that enum values of "_TEST*" are reserved for testing and
* should not be changed without changing the associated tests.
*
* See mobile/android/base/docs/index.rst for a full dictionary.
*/
@RobocopTarget
public interface TelemetryContract {
/**
* Holds event names. Intended for use with
* Telemetry.sendUIEvent() as the "action" parameter.
*
* Please keep this list sorted.
*/
public enum Event {
// Generic action, usually for tracking menu and toolbar actions.
ACTION("action.1"),
// Cancel a state, action, etc.
CANCEL("cancel.1"),
// Start casting a video.
// Note: Only used in JavaScript for now, but here for completeness.
CAST("cast.1"),
// Editing an item.
EDIT("edit.1"),
// Launching (opening) an external application.
// Note: Only used in JavaScript for now, but here for completeness.
LAUNCH("launch.1"),
// Loading a URL.
LOAD_URL("loadurl.1"),
LOCALE_BROWSER_RESET("locale.browser.reset.1"),
LOCALE_BROWSER_SELECTED("locale.browser.selected.1"),
LOCALE_BROWSER_UNSELECTED("locale.browser.unselected.1"),
// Hide a built-in home panel.
PANEL_HIDE("panel.hide.1"),
// Move a home panel up or down.
PANEL_MOVE("panel.move.1"),
// Remove a custom home panel.
PANEL_REMOVE("panel.remove.1"),
// Set default home panel.
PANEL_SET_DEFAULT("panel.setdefault.1"),
// Show a hidden built-in home panel.
PANEL_SHOW("panel.show.1"),
// Pinning an item.
PIN("pin.1"),
// Outcome of data policy notification: can be true or false.
POLICY_NOTIFICATION_SUCCESS("policynotification.success.1"),
// Sanitizing private data.
SANITIZE("sanitize.1"),
// Saving a resource (reader, bookmark, etc) for viewing later.
SAVE("save.1"),
// Perform a search -- currently used when starting a search in the search activity.
SEARCH("search.1"),
// Remove a search engine.
SEARCH_REMOVE("search.remove.1"),
// Restore default search engines.
SEARCH_RESTORE_DEFAULTS("search.restoredefaults.1"),
// Set default search engine.
SEARCH_SET_DEFAULT("search.setdefault.1"),
// Sharing content.
SHARE("share.1"),
// Show a UI element.
SHOW("show.1"),
// Undoing a user action.
// Note: Only used in JavaScript for now, but here for completeness.
UNDO("undo.1"),
// Unpinning an item.
UNPIN("unpin.1"),
// Stop holding a resource (reader, bookmark, etc) for viewing later.
UNSAVE("unsave.1"),
// When the user performs actions on the in-content network error page.
NETERROR("neterror.1"),
// VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
_TEST1("_test_event_1.1"),
_TEST2("_test_event_2.1"),
_TEST3("_test_event_3.1"),
_TEST4("_test_event_4.1"),
;
private final String string;
Event(final String string) {
this.string = string;
}
@Override
public String toString() {
return string;
}
}
/**
* Holds event methods. Intended for use in
* Telemetry.sendUIEvent() as the "method" parameter.
*
* Please keep this list sorted.
*/
public enum Method {
// Action triggered from the action bar (including the toolbar).
ACTIONBAR("actionbar"),
// Action triggered by hitting the Android back button.
BACK("back"),
// Action triggered from a button.
BUTTON("button"),
// Action taken from a content page -- for example, a search results web page.
CONTENT("content"),
// Action occurred via a context menu.
CONTEXT_MENU("contextmenu"),
// Action triggered from a dialog.
DIALOG("dialog"),
// Action triggered from a doorhanger popup prompt.
DOORHANGER("doorhanger"),
// Action triggered from a view grid item, like a thumbnail.
GRID_ITEM("griditem"),
// Action occurred via an intent.
INTENT("intent"),
// Action occurred via a homescreen launcher.
HOMESCREEN("homescreen"),
// Action triggered from a list.
LIST("list"),
// Action triggered from a view list item, like a row of a list.
LIST_ITEM("listitem"),
// Action occurred via the main menu.
MENU("menu"),
// No method is specified.
NONE(null),
// Action triggered from a notification in the Android notification bar.
NOTIFICATION("notification"),
// Action triggered from a pageaction in the URLBar.
// Note: Only used in JavaScript for now, but here for completeness.
PAGEACTION("pageaction"),
// Action triggered from one of a series of views, such as ViewPager.
PANEL("panel"),
// Action triggered by a background service / automatic system making a decision.
SERVICE("service"),
// Action triggered from a settings screen.
SETTINGS("settings"),
// Actions triggered from the share overlay.
SHARE_OVERLAY("shareoverlay"),
// Action triggered from a suggestion provided to the user.
SUGGESTION("suggestion"),
// Action triggered from an OS system action.
SYSTEM("system"),
// Action triggered from a SuperToast.
// Note: Only used in JavaScript for now, but here for completeness.
TOAST("toast"),
// Action triggerred by pressing a SearchWidget button
WIDGET("widget"),
// VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
_TEST1("_test_method_1"),
_TEST2("_test_method_2"),
;
private final String string;
Method(final String string) {
this.string = string;
}
@Override
public String toString() {
return string;
}
}
/**
* Holds session names. Intended for use with
* Telemetry.startUISession() as the "sessionName" parameter.
*
* Please keep this list sorted.
*/
public enum Session {
// Awesomescreen (including frecency search) is active.
AWESOMESCREEN("awesomescreen.1"),
// Used to tag experiments being run.
EXPERIMENT("experiment.1"),
// Started the very first time we believe the application has been launched.
FIRSTRUN("firstrun.1"),
// Awesomescreen frecency search is active.
FRECENCY("frecency.1"),
// Started when a user enters a given home panel.
// Session name is dynamic, encoded as "homepanel.1:<panel_id>"
HOME_PANEL("homepanel.1"),
// Started when a Reader viewer becomes active in the foreground.
// Note: Only used in JavaScript for now, but here for completeness.
READER("reader.1"),
// Started when the search activity launches.
SEARCH_ACTIVITY("searchactivity.1"),
// Settings activity is active.
SETTINGS("settings.1"),
// VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
_TEST_STARTED_TWICE("_test_session_started_twice.1"),
_TEST_STOPPED_TWICE("_test_session_stopped_twice.1"),
;
private final String string;
Session(final String string) {
this.string = string;
}
@Override
public String toString() {
return string;
}
}
/**
* Holds reasons for stopping a session. Intended for use in
* Telemetry.stopUISession() as the "reason" parameter.
*
* Please keep this list sorted.
*/
public enum Reason {
// Changes were committed.
COMMIT("commit"),
// No reason is specified.
NONE(null),
// VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
_TEST1("_test_reason_1"),
_TEST2("_test_reason_2"),
_TEST_IGNORED("_test_reason_ignored"),
;
private final String string;
Reason(final String string) {
this.string = string;
}
@Override
public String toString() {
return string;
}
}
}

View file

@ -0,0 +1,246 @@
/* -*- 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;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.util.ResourceDrawableUtils;
import org.mozilla.gecko.mozglue.DirectBufferAllocator;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.util.Log;
import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
* Helper class to generate thumbnails for tabs.
* Internally, a queue of pending thumbnails is maintained in mPendingThumbnails.
* The head of the queue is the thumbnail that is currently being processed; upon
* completion of the current thumbnail the next one is automatically processed.
* Changes to the thumbnail width are stashed in mPendingWidth and the change is
* applied between thumbnail processing. This allows a single thumbnail buffer to
* be used for all thumbnails.
*/
public final class ThumbnailHelper {
private static final boolean DEBUG = false;
private static final String LOGTAG = "GeckoThumbnailHelper";
public static final float TABS_PANEL_THUMBNAIL_ASPECT_RATIO = 0.8333333f;
public static final float TOP_SITES_THUMBNAIL_ASPECT_RATIO = 0.571428571f; // this is a 4:7 ratio (as per UX decision)
public static final float THUMBNAIL_ASPECT_RATIO;
static {
// As we only want to generate one thumbnail for each tab, we calculate the
// largest aspect ratio required and create the thumbnail based off that.
// Any views with a smaller aspect ratio will use a cropped version of the
// same image.
THUMBNAIL_ASPECT_RATIO = Math.max(TABS_PANEL_THUMBNAIL_ASPECT_RATIO, TOP_SITES_THUMBNAIL_ASPECT_RATIO);
}
public enum CachePolicy {
STORE,
NO_STORE
}
// static singleton stuff
private static ThumbnailHelper sInstance;
public static synchronized ThumbnailHelper getInstance() {
if (sInstance == null) {
sInstance = new ThumbnailHelper();
}
return sInstance;
}
// instance stuff
private final ArrayList<Tab> mPendingThumbnails; // synchronized access only
private volatile int mPendingWidth;
private int mWidth;
private int mHeight;
private ByteBuffer mBuffer;
private ThumbnailHelper() {
final Resources res = GeckoAppShell.getContext().getResources();
mPendingThumbnails = new ArrayList<>();
try {
mPendingWidth = (int) res.getDimension(R.dimen.tab_thumbnail_width);
} catch (Resources.NotFoundException nfe) {
}
mWidth = -1;
mHeight = -1;
}
public void getAndProcessThumbnailFor(final int tabId, final ResourceDrawableUtils.BitmapLoader loader) {
final Tab tab = Tabs.getInstance().getTab(tabId);
if (tab != null) {
getAndProcessThumbnailFor(tab, loader);
}
}
public void getAndProcessThumbnailFor(final Tab tab, final ResourceDrawableUtils.BitmapLoader loader) {
ResourceDrawableUtils.runOnBitmapFoundOnUiThread(loader, tab.getThumbnail());
Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() {
@Override
public void onTabChanged(final Tab t, final Tabs.TabEvents msg, final String data) {
if (tab != t || msg != Tabs.TabEvents.THUMBNAIL) {
return;
}
Tabs.unregisterOnTabsChangedListener(this);
ResourceDrawableUtils.runOnBitmapFoundOnUiThread(loader, t.getThumbnail());
}
});
getAndProcessThumbnailFor(tab);
}
public void getAndProcessThumbnailFor(Tab tab) {
if (AboutPages.isAboutHome(tab.getURL()) || AboutPages.isAboutPrivateBrowsing(tab.getURL())) {
tab.updateThumbnail(null, CachePolicy.NO_STORE);
return;
}
synchronized (mPendingThumbnails) {
if (mPendingThumbnails.lastIndexOf(tab) > 0) {
// This tab is already in the queue, so don't add it again.
// Note that if this tab is only at the *head* of the queue,
// (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do
// add it again because it may have already been thumbnailed
// and now we need to do it again.
return;
}
mPendingThumbnails.add(tab);
if (mPendingThumbnails.size() > 1) {
// Some thumbnail was already being processed, so wait
// for that to be done.
return;
}
requestThumbnailLocked(tab);
}
}
public void setThumbnailWidth(int width) {
// Check inverted for safety: Bug 803299 Comment 34.
if (GeckoAppShell.getScreenDepth() == 24) {
mPendingWidth = width;
} else {
// Bug 776906: on 16-bit screens we need to ensure an even width.
mPendingWidth = (width + 1) & (~1);
}
}
private void updateThumbnailSizeLocked() {
// Apply any pending width updates.
mWidth = mPendingWidth;
mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO);
int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
int capacity = mWidth * mHeight * pixelSize;
if (DEBUG) {
Log.d(LOGTAG, "Using new thumbnail size: " + capacity +
" (width " + mWidth + " - height " + mHeight + ")");
}
if (mBuffer == null || mBuffer.capacity() != capacity) {
if (mBuffer != null) {
mBuffer = DirectBufferAllocator.free(mBuffer);
}
try {
mBuffer = DirectBufferAllocator.allocate(capacity);
} catch (IllegalArgumentException iae) {
Log.w(LOGTAG, iae.toString());
} catch (OutOfMemoryError oom) {
Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity);
}
// If we hit an error above, mBuffer will be pointing to null, so we are in a sane state.
}
}
private void requestThumbnailLocked(Tab tab) {
updateThumbnailSizeLocked();
if (mBuffer == null) {
// Buffer allocation may have failed. In this case we can't send the
// event requesting the screenshot which means we won't get back a response
// and so our queue will grow unboundedly. Handle this scenario by clearing
// the queue (no point trying more thumbnailing right now since we're likely
// low on memory). We will try again normally on the next call to
// getAndProcessThumbnailFor which will hopefully be when we have more free memory.
mPendingThumbnails.clear();
return;
}
if (DEBUG) {
Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight);
}
requestThumbnailLocked(mBuffer, tab, tab.getId(), mWidth, mHeight);
}
@WrapForJNI(stubName = "RequestThumbnail", dispatchTo = "proxy")
private static native void requestThumbnailLocked(ByteBuffer data, Tab tab, int tabId,
int width, int height);
/* This method is invoked by JNI once the thumbnail data is ready. */
@WrapForJNI(calledFrom = "gecko")
private static void notifyThumbnail(final ByteBuffer data, final Tab tab,
final boolean success, final boolean shouldStore) {
final ThumbnailHelper helper = ThumbnailHelper.getInstance();
if (success) {
helper.handleThumbnailData(
tab, data, shouldStore ? CachePolicy.STORE : CachePolicy.NO_STORE);
}
helper.processNextThumbnail();
}
private void processNextThumbnail() {
synchronized (mPendingThumbnails) {
if (mPendingThumbnails.isEmpty()) {
return;
}
mPendingThumbnails.remove(0);
if (!mPendingThumbnails.isEmpty()) {
requestThumbnailLocked(mPendingThumbnails.get(0));
}
}
}
private void handleThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) {
if (DEBUG) {
Log.d(LOGTAG, "handleThumbnailData: " + data.capacity());
}
if (data != mBuffer) {
// This should never happen, but log it and recover gracefully
Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!");
}
processThumbnailData(tab, data, cachePolicy);
}
private void processThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) {
Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight);
data.position(0);
b.copyPixelsFromBuffer(data);
setTabThumbnail(tab, b, null, cachePolicy);
}
private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed, CachePolicy cachePolicy) {
if (bitmap == null) {
if (compressed == null) {
Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!");
return;
}
bitmap = BitmapUtils.decodeByteArray(compressed);
}
tab.updateThumbnail(bitmap, cachePolicy);
}
}

View file

@ -0,0 +1,838 @@
/* -*- 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;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.gfx.LayerView;
import org.mozilla.gecko.gfx.PanZoomController;
import org.mozilla.gecko.gfx.PointUtils;
import org.mozilla.gecko.mozglue.DirectBufferAllocator;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
public class ZoomedView extends FrameLayout implements LayerView.DynamicToolbarListener,
LayerView.ZoomedViewListener, GeckoEventListener {
private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName();
private static final float[] ZOOM_FACTORS_LIST = {2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 1.5f};
private static final int W_CAPTURED_VIEW_IN_PERCENT = 50;
private static final int H_CAPTURED_VIEW_IN_PERCENT = 50;
private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000;
private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000;
private static final int OPENING_ANIMATION_DURATION_MS = 250;
private static final int CLOSING_ANIMATION_DURATION_MS = 150;
private static final float OVERSHOOT_INTERPOLATOR_TENSION = 1.5f;
private float zoomFactor;
private int currentZoomFactorIndex;
private boolean isSimplifiedUI;
private int defaultZoomFactor;
private PrefsHelper.PrefHandler prefObserver;
private ImageView zoomedImageView;
private LayerView layerView;
private int viewWidth;
private int viewHeight; // Only the zoomed view height, no toolbar, no shadow ...
private int viewContainerWidth;
private int viewContainerHeight; // Zoomed view height with toolbar and other elements like shadow, ...
private int containterSize; // shadow, margin, ...
private Point lastPosition;
private boolean shouldSetVisibleOnUpdate;
private boolean isBlockedFromAppearing; // Prevent the display of the zoomedview while FormAssistantPopup is visible
private PointF returnValue;
private final PointF animationStart;
private ImageView closeButton;
private TextView changeZoomFactorButton;
private boolean toolbarOnTop;
private float offsetDueToToolBarPosition;
private int toolbarHeight;
private int cornerRadius;
private float dynamicToolbarOverlap;
private boolean stopUpdateView;
private int lastOrientation;
private ByteBuffer buffer;
private Runnable requestRenderRunnable;
private long startTimeReRender;
private long lastStartTimeReRender;
private ZoomedViewTouchListener touchListener;
private enum StartPointUpdate {
GECKO_POSITION, CENTER, NO_CHANGE
}
private class RoundedBitmapDrawable extends BitmapDrawable {
private Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
final float cornerRadius;
final boolean squareOnTopOfDrawable;
RoundedBitmapDrawable(Resources res, Bitmap bitmap, boolean squareOnTop, int radius) {
super(res, bitmap);
squareOnTopOfDrawable = squareOnTop;
final BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP);
paint.setAntiAlias(true);
paint.setShader(shader);
cornerRadius = radius;
}
@Override
public void draw(Canvas canvas) {
int height = getBounds().height();
int width = getBounds().width();
RectF rect = new RectF(0.0f, 0.0f, width, height);
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);
//draw rectangles over the corners we want to be square
if (squareOnTopOfDrawable) {
canvas.drawRect(0, 0, cornerRadius, cornerRadius, paint);
canvas.drawRect(width - cornerRadius, 0, width, cornerRadius, paint);
} else {
canvas.drawRect(0, height - cornerRadius, cornerRadius, height, paint);
canvas.drawRect(width - cornerRadius, height - cornerRadius, width, height, paint);
}
}
}
private class ZoomedViewTouchListener implements View.OnTouchListener {
private float originRawX;
private float originRawY;
private boolean dragged;
private MotionEvent actionDownEvent;
@Override
public boolean onTouch(View view, MotionEvent event) {
if (layerView == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
if (moveZoomedView(event)) {
dragged = true;
}
break;
case MotionEvent.ACTION_UP:
if (dragged) {
dragged = false;
} else {
if (isClickInZoomedView(event.getY())) {
GeckoAppShell.notifyObservers("Gesture:ClickInZoomedView", "");
layerView.dispatchTouchEvent(actionDownEvent);
actionDownEvent.recycle();
PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
// the LayerView expects the coordinates relative to the window, not the surface, so we need
// to adjust that here.
convertedPosition.y += layerView.getSurfaceTranslation();
MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y,
event.getMetaState());
layerView.dispatchTouchEvent(e);
e.recycle();
}
}
break;
case MotionEvent.ACTION_DOWN:
dragged = false;
originRawX = event.getRawX();
originRawY = event.getRawY();
PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
// the LayerView expects the coordinates relative to the window, not the surface, so we need
// to adjust that here.
convertedPosition.y += layerView.getSurfaceTranslation();
actionDownEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
MotionEvent.ACTION_DOWN, convertedPosition.x, convertedPosition.y,
event.getMetaState());
break;
}
return true;
}
private boolean isClickInZoomedView(float y) {
return ((toolbarOnTop && y > toolbarHeight) ||
(!toolbarOnTop && y < ZoomedView.this.viewHeight));
}
private boolean moveZoomedView(MotionEvent event) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) ZoomedView.this.getLayoutParams();
if ((!dragged) && (Math.abs((int) (event.getRawX() - originRawX)) < PanZoomController.CLICK_THRESHOLD)
&& (Math.abs((int) (event.getRawY() - originRawY)) < PanZoomController.CLICK_THRESHOLD)) {
// When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position.
// In this case, the move is ignored if the delta is lower than 1 unit.
return false;
}
float newLeftMargin = params.leftMargin + event.getRawX() - originRawX;
float newTopMargin = params.topMargin + event.getRawY() - originRawY;
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin, StartPointUpdate.CENTER);
originRawX = event.getRawX();
originRawY = event.getRawY();
return true;
}
}
public ZoomedView(Context context) {
this(context, null, 0);
}
public ZoomedView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ZoomedView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
isSimplifiedUI = true;
isBlockedFromAppearing = false;
getPrefs();
currentZoomFactorIndex = 0;
returnValue = new PointF();
animationStart = new PointF();
requestRenderRunnable = new Runnable() {
@Override
public void run() {
requestZoomedViewRender();
}
};
touchListener = new ZoomedViewTouchListener();
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange",
"Gesture:CloseZoomedView", "Browser:ZoomToPageWidth", "Browser:ZoomToRect",
"FormAssist:AutoComplete", "FormAssist:Hide");
}
void destroy() {
if (prefObserver != null) {
PrefsHelper.removeObserver(prefObserver);
prefObserver = null;
}
ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange",
"Gesture:CloseZoomedView", "Browser:ZoomToPageWidth", "Browser:ZoomToRect",
"FormAssist:AutoComplete", "FormAssist:Hide");
}
// This method (onFinishInflate) is called only when the zoomed view class is used inside
// an xml structure <org.mozilla.gecko.ZoomedView ...
// It won't be called if the class is used from java code like "new ZoomedView(context);"
@Override
protected void onFinishInflate() {
super.onFinishInflate();
closeButton = (ImageView) findViewById(R.id.dialog_close);
changeZoomFactorButton = (TextView) findViewById(R.id.change_zoom_factor);
zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
updateUI();
toolbarHeight = getResources().getDimensionPixelSize(R.dimen.zoomed_view_toolbar_height);
containterSize = getResources().getDimensionPixelSize(R.dimen.drawable_dropshadow_size);
cornerRadius = getResources().getDimensionPixelSize(R.dimen.standard_corner_radius);
moveToolbar(true);
}
private void setListeners() {
closeButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
stopZoomDisplay(true);
}
});
changeZoomFactorButton.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (event.getX() >= (changeZoomFactorButton.getLeft() + changeZoomFactorButton.getWidth() / 2)) {
changeZoomFactor(true);
} else {
changeZoomFactor(false);
}
}
return true;
}
});
setOnTouchListener(touchListener);
}
private void removeListeners() {
closeButton.setOnClickListener(null);
changeZoomFactorButton.setOnTouchListener(null);
setOnTouchListener(null);
}
/*
* Convert a click from ZoomedView. Return the position of the click in the
* LayerView
*/
private PointF getUnzoomedPositionFromPointInZoomedView(float x, float y) {
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
final float parentWidth = metrics.getWidth();
final float parentHeight = metrics.getHeight();
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
// The number of unzoomed content pixels that can be displayed in the
// zoomed area.
float visibleContentPixels = viewWidth / zoomFactor;
// The offset in content pixels of the leftmost zoomed pixel from the
// layerview's left edge when the zoomed view is moved to the right as
// far as it can go.
float maxContentOffset = parentWidth - visibleContentPixels;
// The maximum offset in screen pixels that the zoomed view can have
float maxZoomedViewOffset = parentWidth - viewContainerWidth;
// The above values allow us to compute the term
// maxContentOffset / maxZoomedViewOffset
// which is the number of content pixels that we should move over by
// for every screen pixel that the zoomed view is moved over by.
// This allows a smooth transition from when the zoomed view is at the
// leftmost extent to when it is at the rightmost extent.
// This is the offset in content pixels of the leftmost zoomed pixel
// visible in the zoomed view. This value is relative to the layerview
// edge.
float zoomedContentOffset = ((float)params.leftMargin) * maxContentOffset / maxZoomedViewOffset;
returnValue.x = (int)(zoomedContentOffset + (x / zoomFactor));
// Same comments here vertically
visibleContentPixels = viewHeight / zoomFactor;
maxContentOffset = parentHeight - visibleContentPixels;
maxZoomedViewOffset = parentHeight - (viewContainerHeight - toolbarHeight);
float zoomedAreaOffset = (float)params.topMargin + offsetDueToToolBarPosition - layerView.getSurfaceTranslation();
zoomedContentOffset = zoomedAreaOffset * maxContentOffset / maxZoomedViewOffset;
returnValue.y = (int)(zoomedContentOffset + ((y - offsetDueToToolBarPosition) / zoomFactor));
return returnValue;
}
/*
* A touch point (x,y) occurs in LayerView, this point should be displayed
* in the center of the zoomed view. The returned point is the position of
* the Top-Left zoomed view point on the screen device
*/
private PointF getZoomedViewTopLeftPositionFromTouchPosition(float x, float y) {
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
final float parentWidth = metrics.getWidth();
final float parentHeight = metrics.getHeight();
// See comments in getUnzoomedPositionFromPointInZoomedView, but the
// transformations here are largely the reverse of that function.
float visibleContentPixels = viewWidth / zoomFactor;
float maxContentOffset = parentWidth - visibleContentPixels;
float maxZoomedViewOffset = parentWidth - viewContainerWidth;
float contentPixelOffset = x - (visibleContentPixels / 2.0f);
returnValue.x = (int)(contentPixelOffset * (maxZoomedViewOffset / maxContentOffset));
visibleContentPixels = viewHeight / zoomFactor;
maxContentOffset = parentHeight - visibleContentPixels;
maxZoomedViewOffset = parentHeight - (viewContainerHeight - toolbarHeight);
contentPixelOffset = y - (visibleContentPixels / 2.0f);
float unscaledViewOffset = layerView.getSurfaceTranslation() - offsetDueToToolBarPosition;
returnValue.y = (int)((contentPixelOffset * (maxZoomedViewOffset / maxContentOffset)) + unscaledViewOffset);
return returnValue;
}
private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin,
StartPointUpdate animateStartPoint) {
RelativeLayout.LayoutParams newLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
newLayoutParams.leftMargin = (int) newLeftMargin;
newLayoutParams.topMargin = (int) newTopMargin;
int topMarginMin = (int)(layerView.getSurfaceTranslation() + dynamicToolbarOverlap);
int topMarginMax = layerView.getHeight() - viewContainerHeight;
int leftMarginMin = 0;
int leftMarginMax = layerView.getWidth() - viewContainerWidth;
if (newTopMargin < topMarginMin) {
newLayoutParams.topMargin = topMarginMin;
} else if (newTopMargin > topMarginMax) {
newLayoutParams.topMargin = topMarginMax;
}
if (newLeftMargin < leftMarginMin) {
newLayoutParams.leftMargin = leftMarginMin;
} else if (newLeftMargin > leftMarginMax) {
newLayoutParams.leftMargin = leftMarginMax;
}
if (newLayoutParams.topMargin < topMarginMin + 1) {
moveToolbar(false);
} else if (newLayoutParams.topMargin > topMarginMax - 1) {
moveToolbar(true);
}
if (animateStartPoint == StartPointUpdate.GECKO_POSITION) {
// Before this point, the animationStart point is relative to the layerView.
// The value is initialized in startZoomDisplay using the click point position coming from Gecko.
// The position of the zoomed view is now calculated, so the position of the animation
// can now be correctly set relative to the zoomed view
animationStart.x = animationStart.x - newLayoutParams.leftMargin;
animationStart.y = animationStart.y - newLayoutParams.topMargin;
} else if (animateStartPoint == StartPointUpdate.CENTER) {
// At this point, the animationStart point is no more valid probably because
// the zoomed view has been moved by the user.
// In this case, the animationStart point is set to the center point of the zoomed view.
PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(viewContainerWidth / 2, viewContainerHeight / 2);
animationStart.x = convertedPosition.x - newLayoutParams.leftMargin;
animationStart.y = convertedPosition.y - newLayoutParams.topMargin;
}
setLayoutParams(newLayoutParams);
PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, offsetDueToToolBarPosition);
lastPosition = PointUtils.round(convertedPosition);
requestZoomedViewRender();
}
private void moveToolbar(boolean moveTop) {
if (toolbarOnTop == moveTop) {
return;
}
toolbarOnTop = moveTop;
if (toolbarOnTop) {
offsetDueToToolBarPosition = toolbarHeight;
} else {
offsetDueToToolBarPosition = 0;
}
RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) zoomedImageView.getLayoutParams();
RelativeLayout.LayoutParams pChangeZoomFactorButton = (RelativeLayout.LayoutParams) changeZoomFactorButton.getLayoutParams();
RelativeLayout.LayoutParams pCloseButton = (RelativeLayout.LayoutParams) closeButton.getLayoutParams();
if (moveTop) {
p.addRule(RelativeLayout.BELOW, R.id.change_zoom_factor);
pChangeZoomFactorButton.addRule(RelativeLayout.BELOW, 0);
pCloseButton.addRule(RelativeLayout.BELOW, 0);
} else {
p.addRule(RelativeLayout.BELOW, 0);
pChangeZoomFactorButton.addRule(RelativeLayout.BELOW, R.id.zoomed_image_view);
pCloseButton.addRule(RelativeLayout.BELOW, R.id.zoomed_image_view);
}
pChangeZoomFactorButton.addRule(RelativeLayout.ALIGN_LEFT, R.id.zoomed_image_view);
pCloseButton.addRule(RelativeLayout.ALIGN_RIGHT, R.id.zoomed_image_view);
zoomedImageView.setLayoutParams(p);
changeZoomFactorButton.setLayoutParams(pChangeZoomFactorButton);
closeButton.setLayoutParams(pCloseButton);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// In case of orientation change, the zoomed view update is stopped until the orientation change
// is completed. At this time, the function onMetricsChanged is called and the
// zoomed view update is restarted again.
if (lastOrientation != newConfig.orientation) {
shouldBlockUpdate(true);
lastOrientation = newConfig.orientation;
}
}
private void refreshZoomedViewSize(ImmutableViewportMetrics viewport) {
if (layerView == null) {
return;
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
setCapturedSize(viewport);
moveZoomedView(viewport, params.leftMargin, params.topMargin, StartPointUpdate.NO_CHANGE);
}
private void setCapturedSize(ImmutableViewportMetrics metrics) {
float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight());
viewWidth = (int) ((parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
viewHeight = (int) ((parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
viewContainerHeight = viewHeight + toolbarHeight +
2 * containterSize; // Top and bottom shadows
viewContainerWidth = viewWidth +
2 * containterSize; // Right and left shadows
// Display in zoomedview is corrupted when width is an odd number
// More details about this issue here: bug 776906 comment 11
viewWidth &= ~0x1;
}
private void shouldBlockUpdate(boolean shouldBlockUpdate) {
stopUpdateView = shouldBlockUpdate;
}
private Bitmap.Config getBitmapConfig() {
return (GeckoAppShell.getScreenDepth() == 24) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
}
private void updateUI() {
// onFinishInflate is not yet completed, the update of the UI will be done later
if (changeZoomFactorButton == null) {
return;
}
if (isSimplifiedUI) {
changeZoomFactorButton.setVisibility(View.INVISIBLE);
} else {
setTextInZoomFactorButton(zoomFactor);
changeZoomFactorButton.setVisibility(View.VISIBLE);
}
}
private void getPrefs() {
prefObserver = new PrefsHelper.PrefHandlerBase() {
@Override
public void prefValue(String pref, boolean simplified) {
isSimplifiedUI = simplified;
if (simplified) {
zoomFactor = (float) defaultZoomFactor;
} else {
zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
}
updateUI();
}
@Override
public void prefValue(String pref, int defaultZoomFactorFromSettings) {
defaultZoomFactor = defaultZoomFactorFromSettings;
if (isSimplifiedUI) {
zoomFactor = (float) defaultZoomFactor;
} else {
zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
}
updateUI();
}
};
PrefsHelper.addObserver(new String[] { "ui.zoomedview.simplified",
"ui.zoomedview.defaultZoomFactor" },
prefObserver);
}
private void startZoomDisplay(LayerView aLayerView, final int leftFromGecko, final int topFromGecko) {
if (isBlockedFromAppearing) {
return;
}
if (layerView == null) {
layerView = aLayerView;
layerView.addZoomedViewListener(this);
layerView.getDynamicToolbarAnimator().addTranslationListener(this);
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
setCapturedSize(metrics);
}
startTimeReRender = 0;
shouldSetVisibleOnUpdate = true;
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
// At this point, the start point is relative to the layerView.
// Later, it will be converted relative to the zoomed view as soon as
// the position of the zoomed view will be calculated.
animationStart.x = (float) leftFromGecko * metrics.zoomFactor;
animationStart.y = (float) topFromGecko * metrics.zoomFactor + layerView.getSurfaceTranslation();
moveUsingGeckoPosition(leftFromGecko, topFromGecko);
}
public void stopZoomDisplay(boolean withAnimation) {
// If "startZoomDisplay" is running and not totally completed (Gecko thread is still
// running and "showZoomedView" has not yet been called), the zoomed view will be
// displayed after this call and it should not.
// Force the stop of the zoomed view, changing the shouldSetVisibleOnUpdate flag
// before the test of the visibility.
shouldSetVisibleOnUpdate = false;
if (getVisibility() == View.VISIBLE) {
hideZoomedView(withAnimation);
ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
if (layerView != null) {
layerView.getDynamicToolbarAnimator().removeTranslationListener(this);
layerView.removeZoomedViewListener(this);
layerView = null;
}
}
}
private void changeZoomFactor(boolean zoomIn) {
if (zoomIn && currentZoomFactorIndex < ZOOM_FACTORS_LIST.length - 1) {
currentZoomFactorIndex++;
} else if (zoomIn && currentZoomFactorIndex >= ZOOM_FACTORS_LIST.length - 1) {
currentZoomFactorIndex = 0;
} else if (!zoomIn && currentZoomFactorIndex > 0) {
currentZoomFactorIndex--;
} else {
currentZoomFactorIndex = ZOOM_FACTORS_LIST.length - 1;
}
zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
refreshZoomedViewSize(metrics);
setTextInZoomFactorButton(zoomFactor);
}
private void setTextInZoomFactorButton(float zoom) {
final String percentageValue = Integer.toString((int) (100 * zoom));
changeZoomFactorButton.setText("- " + getResources().getString(R.string.percent, percentageValue) + " +");
}
@Override
public void handleMessage(final String event, final JSONObject message) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
try {
if (event.equals("Gesture:clusteredLinksClicked")) {
final JSONObject clickPosition = message.getJSONObject("clickPosition");
int left = clickPosition.getInt("x");
int top = clickPosition.getInt("y");
// Start to display inside the zoomedView
LayerView geckoAppLayerView = GeckoAppShell.getLayerView();
if (geckoAppLayerView != null) {
startZoomDisplay(geckoAppLayerView, left, top);
}
} else if (event.equals("Window:Resize")) {
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
refreshZoomedViewSize(metrics);
} else if (event.equals("Content:LocationChange")) {
stopZoomDisplay(false);
} else if (event.equals("Gesture:CloseZoomedView") ||
event.equals("Browser:ZoomToPageWidth") ||
event.equals("Browser:ZoomToRect")) {
stopZoomDisplay(true);
} else if (event.equals("FormAssist:AutoComplete")) {
isBlockedFromAppearing = true;
stopZoomDisplay(true);
} else if (event.equals("FormAssist:Hide")) {
isBlockedFromAppearing = false;
}
} catch (JSONException e) {
Log.e(LOGTAG, "JSON exception", e);
}
}
});
}
private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) {
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
final float parentHeight = metrics.getHeight();
// moveToolbar is called before getZoomedViewTopLeftPositionFromTouchPosition in order to
// correctly center vertically the zoomed area
moveToolbar((topFromGecko * metrics.zoomFactor > parentHeight / 2));
PointF convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor),
(topFromGecko * metrics.zoomFactor));
moveZoomedView(metrics, convertedPosition.x, convertedPosition.y, StartPointUpdate.GECKO_POSITION);
}
@Override
public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation) {
ThreadUtils.assertOnUiThread();
if (layerView != null) {
dynamicToolbarOverlap = aLayerViewTranslation - aToolbarTranslation;
refreshZoomedViewSize(layerView.getViewportMetrics());
}
}
@Override
public void onMetricsChanged(final ImmutableViewportMetrics viewport) {
// It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient).
// Post to UI Thread to avoid Exception:
// "Only the original thread that created a view hierarchy can touch its views."
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
shouldBlockUpdate(false);
refreshZoomedViewSize(viewport);
}
});
}
@Override
public void onPanZoomStopped() {
}
@Override
public void updateView(ByteBuffer data) {
final Bitmap sb3 = Bitmap.createBitmap(viewWidth, viewHeight, getBitmapConfig());
if (sb3 != null) {
data.rewind();
try {
sb3.copyPixelsFromBuffer(data);
} catch (Exception iae) {
Log.w(LOGTAG, iae.toString());
}
if (zoomedImageView != null) {
RoundedBitmapDrawable ob3 = new RoundedBitmapDrawable(getResources(), sb3, toolbarOnTop, cornerRadius);
zoomedImageView.setImageDrawable(ob3);
}
}
if (shouldSetVisibleOnUpdate) {
this.showZoomedView();
}
lastStartTimeReRender = startTimeReRender;
startTimeReRender = 0;
}
private void showZoomedView() {
// no animation if the zoomed view is already visible
if (getVisibility() != View.VISIBLE) {
final Animation anim = new ScaleAnimation(
0f, 1f, // Start and end values for the X axis scaling
0f, 1f, // Start and end values for the Y axis scaling
Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling
Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling
anim.setFillAfter(true); // Needed to keep the result of the animation
anim.setDuration(OPENING_ANIMATION_DURATION_MS);
anim.setInterpolator(new OvershootInterpolator(OVERSHOOT_INTERPOLATOR_TENSION));
anim.setAnimationListener(new AnimationListener() {
public void onAnimationEnd(Animation animation) {
setListeners();
}
public void onAnimationRepeat(Animation animation) {
}
public void onAnimationStart(Animation animation) {
removeListeners();
}
});
setAnimation(anim);
}
setVisibility(View.VISIBLE);
shouldSetVisibleOnUpdate = false;
}
private void hideZoomedView(boolean withAnimation) {
if (withAnimation) {
final Animation anim = new ScaleAnimation(
1f, 0f, // Start and end values for the X axis scaling
1f, 0f, // Start and end values for the Y axis scaling
Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling
Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling
anim.setFillAfter(true); // Needed to keep the result of the animation
anim.setDuration(CLOSING_ANIMATION_DURATION_MS);
anim.setAnimationListener(new AnimationListener() {
public void onAnimationEnd(Animation animation) {
}
public void onAnimationRepeat(Animation animation) {
}
public void onAnimationStart(Animation animation) {
removeListeners();
}
});
setAnimation(anim);
} else {
removeListeners();
setAnimation(null);
}
setVisibility(View.GONE);
shouldSetVisibleOnUpdate = false;
}
private void updateBufferSize() {
int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
int capacity = viewWidth * viewHeight * pixelSize;
if (buffer == null || buffer.capacity() != capacity) {
buffer = DirectBufferAllocator.free(buffer);
buffer = DirectBufferAllocator.allocate(capacity);
}
}
private boolean isRendering() {
return (startTimeReRender != 0);
}
private boolean renderFrequencyTooHigh() {
return ((System.nanoTime() - lastStartTimeReRender) < MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS);
}
@WrapForJNI(dispatchTo = "gecko")
private static native void requestZoomedViewData(ByteBuffer buffer, int tabId,
int xPos, int yPos, int width,
int height, float scale);
@Override
public void requestZoomedViewRender() {
if (stopUpdateView) {
return;
}
// remove pending runnable
ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
// "requestZoomedViewRender" can be called very often by Gecko (endDrawing in LayerRender) without
// any thing changed in the zoomed area (useless calls from the "zoomed area" point of view).
// "requestZoomedViewRender" can take time to re-render the zoomed view, it depends of the complexity
// of the html on this area.
// To avoid to slow down the application, the 2 following cases are tested:
// 1- Last render is still running, plan another render later.
if (isRendering()) {
// post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later
// We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done.
// For a static html page WITHOUT any animation/video, there is a last call to endDrawing and we need to make
// the zoomed render on this last call.
ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS);
return;
}
// 2- Current render occurs too early, plan another render later.
if (renderFrequencyTooHigh()) {
// post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later
// We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done.
// For a page WITH animation/video, the animation/video can be stopped, and we need to make
// the zoomed render on this last call.
ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS);
return;
}
startTimeReRender = System.nanoTime();
// Allocate the buffer if it's the first call.
// Change the buffer size if it's not the right size.
updateBufferSize();
int tabId = Tabs.getInstance().getSelectedTab().getId();
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
PointF origin = metrics.getOrigin();
final int xPos = (int)origin.x + lastPosition.x;
final int yPos = (int)origin.y + lastPosition.y;
requestZoomedViewData(buffer, tabId, xPos, yPos, viewWidth, viewHeight,
zoomFactor * metrics.zoomFactor);
}
}

View file

@ -0,0 +1,149 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.activitystream;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;
import com.keepsafe.switchboard.SwitchBoard;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Experiments;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.publicsuffix.PublicSuffix;
import java.util.Arrays;
import java.util.List;
public class ActivityStream {
/**
* List of undesired prefixes for labels based on a URL.
*
* This list is by no means complete and is based on those sources:
* - https://gist.github.com/nchapman/36502ad115e8825d522a66549971a3f0
* - https://github.com/mozilla/activity-stream/issues/1311
*/
private static final List<String> UNDESIRED_LABEL_PREFIXES = Arrays.asList(
"index.",
"home."
);
/**
* Undesired labels for labels based on a URL.
*
* This list is by no means complete and is based on those sources:
* - https://gist.github.com/nchapman/36502ad115e8825d522a66549971a3f0
* - https://github.com/mozilla/activity-stream/issues/1311
*/
private static final List<String> UNDESIRED_LABELS = Arrays.asList(
"render",
"login",
"edit"
);
public static boolean isEnabled(Context context) {
if (!isUserEligible(context)) {
// If the user is not eligible then disable activity stream. Even if it has been
// enabled before.
return false;
}
return GeckoSharedPrefs.forApp(context)
.getBoolean(GeckoPreferences.PREFS_ACTIVITY_STREAM, false);
}
/**
* Is the user eligible to use activity stream or should we hide it from settings etc.?
*/
public static boolean isUserEligible(Context context) {
if (AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) {
// If the build flag is enabled then just show the option to the user.
return true;
}
if (AppConstants.NIGHTLY_BUILD && SwitchBoard.isInExperiment(context, Experiments.ACTIVITY_STREAM)) {
// If this is a nightly build and the user is part of the activity stream experiment then
// the option should be visible too. The experiment is limited to Nightly too but I want
// to make really sure that this isn't riding the trains accidentally.
return true;
}
// For everyone else activity stream is not available yet.
return false;
}
/**
* Query whether we want to display Activity Stream as a Home Panel (within the HomePager),
* or as a HomePager replacement.
*/
public static boolean isHomePanel() {
return true;
}
/**
* Extract a label from a URL to use in Activity Stream.
*
* This method implements the proposal from this desktop AS issue:
* https://github.com/mozilla/activity-stream/issues/1311
*
* @param usePath Use the path of the URL to extract a label (if suitable)
*/
public static void extractLabel(final Context context, final String url, final boolean usePath, final LabelCallback callback) {
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
if (TextUtils.isEmpty(url)) {
return "";
}
final Uri uri = Uri.parse(url);
// Use last path segment if suitable
if (usePath) {
final String segment = uri.getLastPathSegment();
if (!TextUtils.isEmpty(segment)
&& !UNDESIRED_LABELS.contains(segment)
&& !segment.matches("^[0-9]+$")) {
boolean hasUndesiredPrefix = false;
for (int i = 0; i < UNDESIRED_LABEL_PREFIXES.size(); i++) {
if (segment.startsWith(UNDESIRED_LABEL_PREFIXES.get(i))) {
hasUndesiredPrefix = true;
break;
}
}
if (!hasUndesiredPrefix) {
return segment;
}
}
}
// If no usable path segment was found then use the host without public suffix and common subdomains
final String host = uri.getHost();
if (TextUtils.isEmpty(host)) {
return url;
}
return StringUtils.stripCommonSubdomains(
PublicSuffix.stripPublicSuffix(context, host));
}
@Override
protected void onPostExecute(String label) {
callback.onLabelExtracted(label);
}
}.execute();
}
public abstract static class LabelCallback {
public abstract void onLabelExtracted(String label);
}
}

View file

@ -0,0 +1,52 @@
package org.mozilla.gecko.adjust;
import android.content.SharedPreferences;
import android.os.Bundle;
import org.mozilla.gecko.AdjustConstants;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.delegates.BrowserAppDelegate;
import org.mozilla.gecko.mozglue.SafeIntent;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.util.IntentUtils;
public class AdjustBrowserAppDelegate extends BrowserAppDelegate {
private final AdjustHelperInterface adjustHelper;
private final AttributionHelperListener attributionHelperListener;
public AdjustBrowserAppDelegate(AttributionHelperListener attributionHelperListener) {
this.adjustHelper = AdjustConstants.getAdjustHelper();
this.attributionHelperListener = attributionHelperListener;
}
@Override
public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
adjustHelper.onCreate(browserApp,
AdjustConstants.MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN,
attributionHelperListener);
final boolean isInAutomation = IntentUtils.getIsInAutomationFromEnvironment(
new SafeIntent(browserApp.getIntent()));
final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp);
// Adjust stores enabled state so this is only necessary because users may have set
// their data preferences before this feature was implemented and we need to respect
// those before upload can occur in Adjust.onResume.
adjustHelper.setEnabled(!isInAutomation
&& prefs.getBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true));
}
@Override
public void onResume(BrowserApp browserApp) {
// Needed for Adjust to get accurate session measurements
adjustHelper.onResume();
}
@Override
public void onPause(BrowserApp browserApp) {
// Needed for Adjust to get accurate session measurements
adjustHelper.onPause();
}
}

View file

@ -0,0 +1,75 @@
/* -*- 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.adjust;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.adjust.sdk.Adjust;
import com.adjust.sdk.AdjustAttribution;
import com.adjust.sdk.AdjustConfig;
import com.adjust.sdk.AdjustReferrerReceiver;
import com.adjust.sdk.LogLevel;
import com.adjust.sdk.OnAttributionChangedListener;
import org.mozilla.gecko.AppConstants;
public class AdjustHelper implements AdjustHelperInterface, OnAttributionChangedListener {
private static final String LOGTAG = AdjustHelper.class.getSimpleName();
private AttributionHelperListener attributionListener;
public void onCreate(final Context context, final String maybeAppToken, final AttributionHelperListener listener) {
final String environment;
final LogLevel logLevel;
if (AppConstants.MOZILLA_OFFICIAL) {
environment = AdjustConfig.ENVIRONMENT_PRODUCTION;
logLevel = LogLevel.WARN;
} else {
environment = AdjustConfig.ENVIRONMENT_SANDBOX;
logLevel = LogLevel.VERBOSE;
}
if (maybeAppToken == null) {
// We've got install tracking turned on -- we better have a token!
throw new IllegalArgumentException("maybeAppToken must not be null");
}
attributionListener = listener;
AdjustConfig config = new AdjustConfig(context, maybeAppToken, environment);
config.setLogLevel(logLevel);
config.setOnAttributionChangedListener(this);
Adjust.onCreate(config);
}
public void onPause() {
Adjust.onPause();
}
public void onResume() {
Adjust.onResume();
}
public void setEnabled(final boolean isEnabled) {
Adjust.setEnabled(isEnabled);
}
public void onReceive(final Context context, final Intent intent) {
new AdjustReferrerReceiver().onReceive(context, intent);
}
@Override
public void onAttributionChanged(AdjustAttribution attribution) {
if (attributionListener == null) {
throw new IllegalStateException("Expected non-null attribution listener.");
}
if (attribution == null) {
Log.e(LOGTAG, "Adjust attribution is null; skipping campaign id retrieval.");
return;
}
attributionListener.onCampaignIdChanged(attribution.campaign);
}
}

View file

@ -0,0 +1,22 @@
/* -*- 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.adjust;
import android.content.Context;
import android.content.Intent;
public interface AdjustHelperInterface {
/**
* Register the Application with the Adjust SDK.
* @param appToken the (secret!) Adjust SDK per-application token to register with; may be null.
*/
void onCreate(final Context context, final String appToken, final AttributionHelperListener listener);
void onPause();
void onResume();
void setEnabled(final boolean isEnabled);
void onReceive(final Context context, final Intent intent);
}

View file

@ -0,0 +1,17 @@
/* -*- 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.adjust;
/**
* Because of how our build module dependencies are structured, we aren't able to use
* the {@link com.adjust.sdk.OnAttributionChangedListener} directly outside of {@link AdjustHelper}.
* If the Adjust SDK is enabled, this listener should be notified when {@link com.adjust.sdk.OnAttributionChangedListener}
* is fired (i.e. this listener would be daisy-chained to the Adjust one). The listener also
* inherits thread-safety from GeckoSharedPrefs which is used to store the campaign ID.
*/
public interface AttributionHelperListener {
void onCampaignIdChanged(String campaignId);
}

View file

@ -0,0 +1,31 @@
/* -*- 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.adjust;
import android.content.Context;
import android.content.Intent;
public class StubAdjustHelper implements AdjustHelperInterface {
public void onCreate(final Context context, final String appToken, final AttributionHelperListener listener) {
// Do nothing.
}
public void onPause() {
// Do nothing.
}
public void onResume() {
// Do nothing.
}
public void setEnabled(final boolean isEnabled) {
// Do nothing.
}
public void onReceive(final Context context, final Intent intent) {
// Do nothing.
}
}

View file

@ -0,0 +1,21 @@
/* -*- 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.animation;
import android.content.Context;
public class AnimationUtils {
private static long mShortDuration = -1;
public static long getShortDuration(Context context) {
if (mShortDuration < 0) {
mShortDuration = context.getResources().getInteger(android.R.integer.config_shortAnimTime);
}
return mShortDuration;
}
}

View file

@ -0,0 +1,27 @@
/* 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.animation;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;
public class HeightChangeAnimation extends Animation {
int mFromHeight;
int mToHeight;
View mView;
public HeightChangeAnimation(View view, int fromHeight, int toHeight) {
mView = view;
mFromHeight = fromHeight;
mToHeight = toHeight;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
mView.getLayoutParams().height = Math.round((mFromHeight * (1 - interpolatedTime)) + (mToHeight * interpolatedTime));
mView.requestLayout();
}
}

View file

@ -0,0 +1,342 @@
/* -*- 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.animation;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.AppConstants.Versions;
import android.os.Handler;
import android.support.v4.view.ViewCompat;
import android.view.Choreographer;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
public class PropertyAnimator implements Runnable {
private static final String LOGTAG = "GeckoPropertyAnimator";
public static enum Property {
ALPHA,
TRANSLATION_X,
TRANSLATION_Y,
SCROLL_X,
SCROLL_Y,
WIDTH,
HEIGHT
}
private class ElementHolder {
View view;
Property property;
float from;
float to;
}
public static interface PropertyAnimationListener {
public void onPropertyAnimationStart();
public void onPropertyAnimationEnd();
}
private final Interpolator mInterpolator;
private long mStartTime;
private final long mDuration;
private final float mDurationReciprocal;
private final List<ElementHolder> mElementsList;
private List<PropertyAnimationListener> mListeners;
FramePoster mFramePoster;
private boolean mUseHardwareLayer;
public PropertyAnimator(long duration) {
this(duration, new DecelerateInterpolator());
}
public PropertyAnimator(long duration, Interpolator interpolator) {
mDuration = duration;
mDurationReciprocal = 1.0f / mDuration;
mInterpolator = interpolator;
mElementsList = new ArrayList<ElementHolder>();
mFramePoster = FramePoster.create(this);
mUseHardwareLayer = true;
}
public void setUseHardwareLayer(boolean useHardwareLayer) {
mUseHardwareLayer = useHardwareLayer;
}
public void attach(View view, Property property, float to) {
ElementHolder element = new ElementHolder();
element.view = view;
element.property = property;
element.to = to;
mElementsList.add(element);
}
public void addPropertyAnimationListener(PropertyAnimationListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<PropertyAnimationListener>();
}
mListeners.add(listener);
}
public long getDuration() {
return mDuration;
}
public long getRemainingTime() {
int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
return mDuration - timePassed;
}
@Override
public void run() {
int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed >= mDuration) {
stop();
return;
}
float interpolation = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
for (ElementHolder element : mElementsList) {
float delta = element.from + ((element.to - element.from) * interpolation);
invalidate(element, delta);
}
mFramePoster.postNextAnimationFrame();
}
public void start() {
if (mDuration == 0) {
return;
}
mStartTime = AnimationUtils.currentAnimationTimeMillis();
// Fix the from value based on current position and property
for (ElementHolder element : mElementsList) {
if (element.property == Property.ALPHA)
element.from = ViewHelper.getAlpha(element.view);
else if (element.property == Property.TRANSLATION_Y)
element.from = ViewHelper.getTranslationY(element.view);
else if (element.property == Property.TRANSLATION_X)
element.from = ViewHelper.getTranslationX(element.view);
else if (element.property == Property.SCROLL_Y)
element.from = ViewHelper.getScrollY(element.view);
else if (element.property == Property.SCROLL_X)
element.from = ViewHelper.getScrollX(element.view);
else if (element.property == Property.WIDTH)
element.from = ViewHelper.getWidth(element.view);
else if (element.property == Property.HEIGHT)
element.from = ViewHelper.getHeight(element.view);
ViewCompat.setHasTransientState(element.view, true);
if (shouldEnableHardwareLayer(element))
element.view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
else
element.view.setDrawingCacheEnabled(true);
}
// Get ViewTreeObserver from any of the participant views
// in the animation.
final ViewTreeObserver treeObserver;
if (mElementsList.size() > 0) {
treeObserver = mElementsList.get(0).view.getViewTreeObserver();
} else {
treeObserver = null;
}
final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (treeObserver.isAlive()) {
treeObserver.removeOnPreDrawListener(this);
}
mFramePoster.postFirstAnimationFrame();
return true;
}
};
// Try to start animation after any on-going layout round
// in the current view tree. OnPreDrawListener seems broken
// on pre-Honeycomb devices, start animation immediatelly
// in this case.
if (treeObserver != null && treeObserver.isAlive()) {
treeObserver.addOnPreDrawListener(preDrawListener);
} else {
mFramePoster.postFirstAnimationFrame();
}
if (mListeners != null) {
for (PropertyAnimationListener listener : mListeners) {
listener.onPropertyAnimationStart();
}
}
}
/**
* Stop the animation, optionally snapping to the end position.
* onPropertyAnimationEnd is only called when snapping to the end position.
*/
public void stop(boolean snapToEndPosition) {
mFramePoster.cancelAnimationFrame();
// Make sure to snap to the end position.
for (ElementHolder element : mElementsList) {
if (snapToEndPosition)
invalidate(element, element.to);
ViewCompat.setHasTransientState(element.view, false);
if (shouldEnableHardwareLayer(element)) {
element.view.setLayerType(View.LAYER_TYPE_NONE, null);
} else {
element.view.setDrawingCacheEnabled(false);
}
}
mElementsList.clear();
if (mListeners != null) {
if (snapToEndPosition) {
for (PropertyAnimationListener listener : mListeners) {
listener.onPropertyAnimationEnd();
}
}
mListeners.clear();
mListeners = null;
}
}
public void stop() {
stop(true);
}
private boolean shouldEnableHardwareLayer(ElementHolder element) {
if (!mUseHardwareLayer) {
return false;
}
if (!(element.view instanceof ViewGroup)) {
return false;
}
if (element.property == Property.ALPHA ||
element.property == Property.TRANSLATION_Y ||
element.property == Property.TRANSLATION_X) {
return true;
}
return false;
}
private void invalidate(final ElementHolder element, final float delta) {
final View view = element.view;
// check to see if the view was detached between the check above and this code
// getting run on the UI thread.
if (view.getHandler() == null)
return;
if (element.property == Property.ALPHA)
ViewHelper.setAlpha(element.view, delta);
else if (element.property == Property.TRANSLATION_Y)
ViewHelper.setTranslationY(element.view, delta);
else if (element.property == Property.TRANSLATION_X)
ViewHelper.setTranslationX(element.view, delta);
else if (element.property == Property.SCROLL_Y)
ViewHelper.scrollTo(element.view, ViewHelper.getScrollX(element.view), (int) delta);
else if (element.property == Property.SCROLL_X)
ViewHelper.scrollTo(element.view, (int) delta, ViewHelper.getScrollY(element.view));
else if (element.property == Property.WIDTH)
ViewHelper.setWidth(element.view, (int) delta);
else if (element.property == Property.HEIGHT)
ViewHelper.setHeight(element.view, (int) delta);
}
private static abstract class FramePoster {
public static FramePoster create(Runnable r) {
if (Versions.feature16Plus) {
return new FramePosterPostJB(r);
}
return new FramePosterPreJB(r);
}
public abstract void postFirstAnimationFrame();
public abstract void postNextAnimationFrame();
public abstract void cancelAnimationFrame();
}
private static class FramePosterPreJB extends FramePoster {
// Default refresh rate in ms.
private static final int INTERVAL = 10;
private final Handler mHandler;
private final Runnable mRunnable;
public FramePosterPreJB(Runnable r) {
mHandler = new Handler();
mRunnable = r;
}
@Override
public void postFirstAnimationFrame() {
mHandler.post(mRunnable);
}
@Override
public void postNextAnimationFrame() {
mHandler.postDelayed(mRunnable, INTERVAL);
}
@Override
public void cancelAnimationFrame() {
mHandler.removeCallbacks(mRunnable);
}
}
private static class FramePosterPostJB extends FramePoster {
private final Choreographer mChoreographer;
private final Choreographer.FrameCallback mCallback;
public FramePosterPostJB(final Runnable r) {
mChoreographer = Choreographer.getInstance();
mCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
r.run();
}
};
}
@Override
public void postFirstAnimationFrame() {
postNextAnimationFrame();
}
@Override
public void postNextAnimationFrame() {
mChoreographer.postFrameCallback(mCallback);
}
@Override
public void cancelAnimationFrame() {
mChoreographer.removeFrameCallback(mCallback);
}
}
}

View file

@ -0,0 +1,97 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.animation;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.graphics.Camera;
import android.graphics.Matrix;
/**
* An animation that rotates the view on the Y axis between two specified angles.
* This animation also adds a translation on the Z axis (depth) to improve the effect.
*/
public class Rotate3DAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
private int mWidth = 1;
private int mHeight = 1;
/**
* Creates a new 3D rotation on the Y axis. The rotation is defined by its
* start angle and its end angle. Both angles are in degrees. The rotation
* is performed around a center point on the 2D space, defined by a pair
* of X and Y coordinates, called centerX and centerY. When the animation
* starts, a translation on the Z axis (depth) is performed. The length
* of the translation can be specified, as well as whether the translation
* should be reversed in time.
*
* @param fromDegrees the start angle of the 3D rotation
* @param toDegrees the end angle of the 3D rotation
* @param centerX the X center of the 3D rotation
* @param centerY the Y center of the 3D rotation
* @param reverse true if the translation should be reversed, false otherwise
*/
public Rotate3DAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
mWidth = width;
mHeight = height;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
camera.rotateX(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-mCenterX * mWidth, -mCenterY * mHeight);
matrix.postTranslate(mCenterX * mWidth, mCenterY * mHeight);
}
}

View file

@ -0,0 +1,109 @@
/* 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.animation;
import android.view.View;
import android.view.ViewGroup;
public final class ViewHelper {
private ViewHelper() {
}
public static float getTranslationX(View view) {
if (view != null) {
return view.getTranslationX();
}
return 0;
}
public static void setTranslationX(View view, float translationX) {
if (view != null) {
view.setTranslationX(translationX);
}
}
public static float getTranslationY(View view) {
if (view != null) {
return view.getTranslationY();
}
return 0;
}
public static void setTranslationY(View view, float translationY) {
if (view != null) {
view.setTranslationY(translationY);
}
}
public static float getAlpha(View view) {
if (view != null) {
return view.getAlpha();
}
return 1;
}
public static void setAlpha(View view, float alpha) {
if (view != null) {
view.setAlpha(alpha);
}
}
public static int getWidth(View view) {
if (view != null) {
return view.getWidth();
}
return 0;
}
public static void setWidth(View view, int width) {
if (view != null) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
lp.width = width;
view.setLayoutParams(lp);
}
}
public static int getHeight(View view) {
if (view != null) {
return view.getHeight();
}
return 0;
}
public static void setHeight(View view, int height) {
if (view != null) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
lp.height = height;
view.setLayoutParams(lp);
}
}
public static int getScrollX(View view) {
if (view != null) {
return view.getScrollX();
}
return 0;
}
public static int getScrollY(View view) {
if (view != null) {
return view.getScrollY();
}
return 0;
}
public static void scrollTo(View view, int scrollX, int scrollY) {
if (view != null) {
view.scrollTo(scrollX, scrollY);
}
}
}

View file

@ -0,0 +1,81 @@
/*
* 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.cleanup;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.support.annotation.VisibleForTesting;
import java.io.File;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* Encapsulates the code to run the {@link FileCleanupService}. Call
* {@link #startIfReady(Context, SharedPreferences, String)} to start the clean-up.
*
* Note: for simplicity, the current implementation does not cache which
* files have been cleaned up and will attempt to delete the same files
* each time it is run. If the file deletion list grows large, consider
* keeping a cache.
*/
public class FileCleanupController {
private static final long MILLIS_BETWEEN_CLEANUPS = TimeUnit.DAYS.toMillis(7);
@VisibleForTesting static final String PREF_LAST_CLEANUP_MILLIS = "cleanup.lastFileCleanupMillis";
// These will be prepended with the path of the profile we're cleaning up.
private static final String[] PROFILE_FILES_TO_CLEANUP = new String[] {
"health.db",
"health.db-journal",
"health.db-shm",
"health.db-wal",
};
/**
* Starts the clean-up if it's time to clean-up, otherwise returns. For simplicity,
* it does not schedule the cleanup for some point in the future - this method will
* have to be called again (i.e. polled) in order to run the clean-up service.
*
* @param context Context of the calling {@link android.app.Activity}
* @param sharedPrefs The {@link SharedPreferences} instance to store the controller state to
* @param profilePath The path to the profile the service should clean-up files from
*/
public static void startIfReady(final Context context, final SharedPreferences sharedPrefs, final String profilePath) {
if (!isCleanupReady(sharedPrefs)) {
return;
}
recordCleanupScheduled(sharedPrefs);
final Intent fileCleanupIntent = new Intent(context, FileCleanupService.class);
fileCleanupIntent.setAction(FileCleanupService.ACTION_DELETE_FILES);
fileCleanupIntent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, getFilesToCleanup(profilePath + "/"));
context.startService(fileCleanupIntent);
}
private static boolean isCleanupReady(final SharedPreferences sharedPrefs) {
final long lastCleanupMillis = sharedPrefs.getLong(PREF_LAST_CLEANUP_MILLIS, -1);
return lastCleanupMillis + MILLIS_BETWEEN_CLEANUPS < System.currentTimeMillis();
}
private static void recordCleanupScheduled(final SharedPreferences sharedPrefs) {
final SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putLong(PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis()).apply();
}
@VisibleForTesting
static ArrayList<String> getFilesToCleanup(final String profilePath) {
final ArrayList<String> out = new ArrayList<>(PROFILE_FILES_TO_CLEANUP.length);
for (final String path : PROFILE_FILES_TO_CLEANUP) {
// Append a file separator, just in-case the caller didn't include one.
out.add(profilePath + File.separator + path);
}
return out;
}
}

View file

@ -0,0 +1,80 @@
/*
* 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.cleanup;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
/**
* An IntentService to delete files.
*
* It takes an {@link ArrayList} of String file paths to delete via the extra
* {@link #EXTRA_FILE_PATHS_TO_DELETE}. If these file paths are directories, they will
* not be traversed recursively and will only be deleted if empty. This is to avoid accidentally
* trashing a users' profile if a folder is accidentally listed.
*
* An IntentService was chosen because:
* * It generally won't be killed when the Activity is
* * (unlike HandlerThread) The system handles scheduling, prioritizing,
* and shutting down the underlying background thread
* * (unlike an existing background thread) We don't block our background operations
* for this, which doesn't directly affect the user.
*
* The major trade-off is that this Service is very dangerous if it's exported... so don't do that!
*/
public class FileCleanupService extends IntentService {
private static final String LOGTAG = "Gecko" + FileCleanupService.class.getSimpleName();
private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
public static final String ACTION_DELETE_FILES = "org.mozilla.gecko.intent.action.DELETE_FILES";
public static final String EXTRA_FILE_PATHS_TO_DELETE = "org.mozilla.gecko.file_paths_to_delete";
public FileCleanupService() {
super(WORKER_THREAD_NAME);
// We're likely to get scheduled again - let's wait until then in order to avoid:
// * The coding complexity of re-running this
// * Consuming system resources: we were probably killed for resource conservation purposes
setIntentRedelivery(false);
}
@Override
protected void onHandleIntent(final Intent intent) {
if (!isIntentValid(intent)) {
return;
}
final ArrayList<String> filesToDelete = intent.getStringArrayListExtra(EXTRA_FILE_PATHS_TO_DELETE);
for (final String path : filesToDelete) {
final File file = new File(path);
file.delete();
}
}
private static boolean isIntentValid(final Intent intent) {
if (intent == null) {
Log.w(LOGTAG, "Received null intent");
return false;
}
if (!intent.getAction().equals(ACTION_DELETE_FILES)) {
Log.w(LOGTAG, "Received unknown intent action: " + intent.getAction());
return false;
}
if (!intent.hasExtra(EXTRA_FILE_PATHS_TO_DELETE)) {
Log.w(LOGTAG, "Received intent with no files extra");
return false;
}
return true;
}
}

View file

@ -0,0 +1,177 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.customtabs;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.util.ColorUtil;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import java.lang.reflect.Field;
import static android.support.customtabs.CustomTabsIntent.EXTRA_TOOLBAR_COLOR;
public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedListener {
private static final String LOGTAG = "CustomTabsActivity";
private static final String SAVED_TOOLBAR_COLOR = "SavedToolbarColor";
private static final String SAVED_TOOLBAR_TITLE = "SavedToolbarTitle";
private static final int NO_COLOR = -1;
private Toolbar toolbar;
private ActionBar actionBar;
private int tabId = -1;
private boolean useDomainTitle = true;
private int toolbarColor;
private String toolbarTitle;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
toolbarColor = savedInstanceState.getInt(SAVED_TOOLBAR_COLOR, NO_COLOR);
toolbarTitle = savedInstanceState.getString(SAVED_TOOLBAR_TITLE, AppConstants.MOZ_APP_BASENAME);
} else {
toolbarColor = NO_COLOR;
toolbarTitle = AppConstants.MOZ_APP_BASENAME;
}
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
updateActionBarWithToolbar(toolbar);
try {
// Since we don't create the Toolbar's TextView ourselves, this seems
// to be the only way of changing the ellipsize setting.
Field f = toolbar.getClass().getDeclaredField("mTitleTextView");
f.setAccessible(true);
TextView textView = (TextView) f.get(toolbar);
textView.setEllipsize(TextUtils.TruncateAt.START);
} catch (Exception e) {
// If we can't ellipsize at the start of the title, we shouldn't display the host
// so as to avoid displaying a misleadingly truncated host.
Log.w(LOGTAG, "Failed to get Toolbar TextView, using default title.");
useDomainTitle = false;
}
actionBar = getSupportActionBar();
actionBar.setTitle(toolbarTitle);
updateToolbarColor(toolbar);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
Tabs.registerOnTabsChangedListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
Tabs.unregisterOnTabsChangedListener(this);
}
@Override
public int getLayout() {
return R.layout.customtabs_activity;
}
@Override
protected void onDone() {
finish();
}
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
if (tab == null) {
return;
}
if (tabId >= 0 && tab.getId() != tabId) {
return;
}
if (msg == Tabs.TabEvents.LOCATION_CHANGE) {
tabId = tab.getId();
final Uri uri = Uri.parse(tab.getURL());
String title = null;
if (uri != null) {
title = uri.getHost();
}
if (!useDomainTitle || title == null || title.isEmpty()) {
toolbarTitle = AppConstants.MOZ_APP_BASENAME;
} else {
toolbarTitle = title;
}
actionBar.setTitle(toolbarTitle);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(SAVED_TOOLBAR_COLOR, toolbarColor);
outState.putString(SAVED_TOOLBAR_TITLE, toolbarTitle);
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void updateActionBarWithToolbar(final Toolbar toolbar) {
setSupportActionBar(toolbar);
final ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
}
}
private void updateToolbarColor(final Toolbar toolbar) {
if (toolbarColor == NO_COLOR) {
final int color = getIntent().getIntExtra(EXTRA_TOOLBAR_COLOR, NO_COLOR);
if (color == NO_COLOR) {
return;
}
toolbarColor = color;
}
final int titleTextColor = ColorUtil.getReadableTextColor(toolbarColor);
toolbar.setBackgroundColor(toolbarColor);
toolbar.setTitleTextColor(titleTextColor);
final Window window = getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(ColorUtil.darken(toolbarColor, 0.25));
}
}
}

View file

@ -0,0 +1,65 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.customtabs;
import android.net.Uri;
import android.os.Bundle;
import android.support.customtabs.CustomTabsService;
import android.support.customtabs.CustomTabsSessionToken;
import android.util.Log;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoService;
import java.util.List;
/**
* Custom tabs service external, third-party apps connect to.
*/
public class GeckoCustomTabsService extends CustomTabsService {
private static final String LOGTAG = "GeckoCustomTabsService";
private static final boolean DEBUG = false;
@Override
protected boolean updateVisuals(CustomTabsSessionToken sessionToken, Bundle bundle) {
Log.v(LOGTAG, "updateVisuals()");
return false;
}
@Override
protected boolean warmup(long flags) {
if (DEBUG) {
Log.v(LOGTAG, "warming up...");
}
GeckoService.startGecko(GeckoProfile.initFromArgs(this, null), null, getApplicationContext());
return true;
}
@Override
protected boolean newSession(CustomTabsSessionToken sessionToken) {
Log.v(LOGTAG, "newSession()");
// Pretend session has been started
return true;
}
@Override
protected boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri uri, Bundle bundle, List<Bundle> list) {
Log.v(LOGTAG, "mayLaunchUrl()");
return false;
}
@Override
protected Bundle extraCommand(String commandName, Bundle bundle) {
Log.v(LOGTAG, "extraCommand()");
return null;
}
}

View file

@ -0,0 +1,79 @@
/* 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.db;
import org.mozilla.gecko.annotation.RobocopTarget;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
/**
* The base class for ContentProviders that wish to use a different DB
* for each profile.
*
* This class has logic shared between ordinary per-profile CPs and
* those that wish to share DB connections between CPs.
*/
public abstract class AbstractPerProfileDatabaseProvider extends AbstractTransactionalProvider {
/**
* Extend this to provide access to your own map of shared databases. This
* is a method so that your subclass doesn't collide with others!
*/
protected abstract PerProfileDatabases<? extends SQLiteOpenHelper> getDatabases();
/*
* Fetches a readable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a readable SQLiteDatabase
*/
@Override
protected SQLiteDatabase getReadableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
}
/*
* Fetches a writable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writable SQLiteDatabase
*/
@Override
protected SQLiteDatabase getWritableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
return getDatabases().getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
}
/**
* This method should ONLY be used for testing purposes.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writable SQLiteDatabase
*/
@Override
@RobocopTarget
public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
return getWritableDatabase(uri);
}
}

View file

@ -0,0 +1,328 @@
/* 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.db;
import org.mozilla.gecko.AppConstants.Versions;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
/**
* This abstract class exists to capture some of the transaction-handling
* commonalities in Fennec's DB layer.
*
* In particular, this abstracts DB access, batching, and a particular
* transaction approach.
*
* That approach is: subclasses implement the abstract methods
* {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
* {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
* {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
*
* These are all called expecting a transaction to be established, so failed
* modifications can be rolled-back, and work batched.
*
* If no transaction is established, that's not a problem. Transaction nesting
* can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
*
* The decision of when to begin a transaction is left to the subclasses,
* primarily to avoid the pattern of a transaction being begun, a read occurring,
* and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
* which we don't handle well. Better to avoid starting a transaction too soon!
*
* You are probably interested in some subclasses:
*
* * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
* querying databases that are stored in the user's profile directory.
* * {@link PerProfileDatabaseProvider} is a simple version that only allows a
* single ContentProvider to access each per-profile database.
* * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
* that allows for multiple providers to safely work with the same databases.
*/
@SuppressWarnings("javadoc")
public abstract class AbstractTransactionalProvider extends ContentProvider {
private static final String LOGTAG = "GeckoTransProvider";
private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
protected abstract SQLiteDatabase getWritableDatabase(Uri uri);
public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
/**
* Track whether we're in a batch operation.
*
* When we're in a batch operation, individual write steps won't even try
* to start a transaction... and neither will they attempt to finish one.
*
* Set this to <code>Boolean.TRUE</code> when you're entering a batch --
* a section of code in which {@link ContentProvider} methods will be
* called, but nested transactions should not be started. Callers are
* responsible for beginning and ending the enclosing transaction, and
* for setting this to <code>Boolean.FALSE</code> when done.
*
* This is a ThreadLocal separate from `db.inTransaction` because batched
* operations start transactions independent of individual ContentProvider
* operations. This doesn't work well with the entire concept of this
* abstract class -- that is, automatically beginning and ending transactions
* for each insert/delete/update operation -- and doing so without
* causing arbitrary nesting requires external tracking.
*
* Note that beginWrite takes a DB argument, but we don't differentiate
* between databases in this tracking flag. If your ContentProvider manages
* multiple database transactions within the same thread, you'll need to
* amend this scheme -- but then, you're already doing some serious wizardry,
* so rock on.
*/
final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
private boolean isInBatch() {
final Boolean isInBatch = isInBatchOperation.get();
if (isInBatch == null) {
return false;
}
return isInBatch;
}
/**
* If we're not currently in a transaction, and we should be, start one.
*/
protected void beginWrite(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not bothering with an intermediate write transaction: inside batch operation.");
return;
}
if (!db.inTransaction()) {
trace("beginWrite: beginning transaction.");
db.beginTransaction();
}
}
/**
* If we're not in a batch, but we are in a write transaction, mark it as
* successful.
*/
protected void markWriteSuccessful(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not marking write successful: inside batch operation.");
return;
}
if (db.inTransaction()) {
trace("Marking write transaction successful.");
db.setTransactionSuccessful();
}
}
/**
* If we're not in a batch, but we are in a write transaction,
* end it.
*
* @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
*/
protected void endWrite(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not ending write: inside batch operation.");
return;
}
if (db.inTransaction()) {
trace("endWrite: ending transaction.");
db.endTransaction();
}
}
protected void beginBatch(final SQLiteDatabase db) {
trace("Beginning batch.");
isInBatchOperation.set(Boolean.TRUE);
db.beginTransaction();
}
protected void markBatchSuccessful(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Marking batch successful.");
db.setTransactionSuccessful();
return;
}
Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
throw new IllegalStateException("Not in batch.");
}
protected void endBatch(final SQLiteDatabase db) {
trace("Ending batch.");
db.endTransaction();
isInBatchOperation.set(Boolean.FALSE);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
int deleted = 0;
try {
deleted = deleteInTransaction(uri, selection, selectionArgs);
markWriteSuccessful(db);
} finally {
endWrite(db);
}
if (deleted > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return deleted;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
trace("Calling insert on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
Uri result = null;
try {
result = insertInTransaction(uri, values);
markWriteSuccessful(db);
} catch (SQLException sqle) {
Log.e(LOGTAG, "exception in DB operation", sqle);
} catch (UnsupportedOperationException uoe) {
Log.e(LOGTAG, "don't know how to perform that insert", uoe);
} finally {
endWrite(db);
}
if (result != null) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return result;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
try {
updated = updateInTransaction(uri, values, selection,
selectionArgs);
markWriteSuccessful(db);
} finally {
endWrite(db);
}
if (updated > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return updated;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
if (values == null) {
return 0;
}
int numValues = values.length;
int successes = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
debug("bulkInsert: explicitly starting transaction.");
beginBatch(db);
try {
for (int i = 0; i < numValues; i++) {
insertInTransaction(uri, values[i]);
successes++;
}
trace("Flushing DB bulkinsert...");
markBatchSuccessful(db);
} finally {
debug("bulkInsert: explicitly ending transaction.");
endBatch(db);
}
if (successes > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return successes;
}
/**
* Indicates whether a query should include deleted fields
* based on the URI.
* @param uri query URI
*/
protected static boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
/**
* Indicates whether an insertion should be made if a record doesn't
* exist, based on the URI.
* @param uri query URI
*/
protected static boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
/**
* Indicates whether query is a test based on the URI.
* @param uri query URI
*/
protected static boolean isTest(Uri uri) {
if (uri == null) {
return false;
}
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
protected static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
protected static void trace(String message) {
if (logVerbose) {
Log.v(LOGTAG, message);
}
}
protected static void debug(String message) {
if (logDebug) {
Log.d(LOGTAG, message);
}
}
}

View file

@ -0,0 +1,64 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
// BaseTable provides a basic implementation of a Table for tables that don't require advanced operations during
// insert, delete, update, or query operations. Implementors must still provide onCreate and onUpgrade operations.
public abstract class BaseTable implements Table {
private static final String LOGTAG = "GeckoBaseTable";
private static final boolean DEBUG = false;
protected static void log(String msg) {
if (DEBUG) {
Log.i(LOGTAG, msg);
}
}
// Table implementation
@Override
public Table.ContentProviderInfo[] getContentProviderInfo() {
return new Table.ContentProviderInfo[0];
}
// Returns the name of the table to modify/query
protected abstract String getTable();
// Table implementation
@Override
public Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] columns, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit) {
Cursor c = db.query(getTable(), columns, selection, selectionArgs, groupBy, null, sortOrder, limit);
log("query " + columns + " in " + selection + " = " + c);
return c;
}
@Override
public int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs) {
int updated = db.updateWithOnConflict(getTable(), values, selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
log("update " + values + " in " + selection + " = " + updated);
return updated;
}
@Override
public long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values) {
long inserted = db.insertOrThrow(getTable(), null, values);
log("insert " + values + " = " + inserted);
return inserted;
}
@Override
public int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs) {
int deleted = db.delete(getTable(), selection, selectionArgs);
log("delete " + selection + " = " + deleted);
return deleted;
}
};

View file

@ -0,0 +1,785 @@
/* -*- 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.db;
import org.mozilla.gecko.AppConstants;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.mozilla.gecko.annotation.RobocopTarget;
@RobocopTarget
public class BrowserContract {
public static final String AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.browser";
public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
public static final String PASSWORDS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.passwords";
public static final Uri PASSWORDS_AUTHORITY_URI = Uri.parse("content://" + PASSWORDS_AUTHORITY);
public static final String FORM_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.formhistory";
public static final Uri FORM_HISTORY_AUTHORITY_URI = Uri.parse("content://" + FORM_HISTORY_AUTHORITY);
public static final String TABS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.tabs";
public static final Uri TABS_AUTHORITY_URI = Uri.parse("content://" + TABS_AUTHORITY);
public static final String HOME_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.home";
public static final Uri HOME_AUTHORITY_URI = Uri.parse("content://" + HOME_AUTHORITY);
public static final String PROFILES_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".profiles";
public static final Uri PROFILES_AUTHORITY_URI = Uri.parse("content://" + PROFILES_AUTHORITY);
public static final String READING_LIST_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.readinglist";
public static final Uri READING_LIST_AUTHORITY_URI = Uri.parse("content://" + READING_LIST_AUTHORITY);
public static final String SEARCH_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.searchhistory";
public static final Uri SEARCH_HISTORY_AUTHORITY_URI = Uri.parse("content://" + SEARCH_HISTORY_AUTHORITY);
public static final String LOGINS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.logins";
public static final Uri LOGINS_AUTHORITY_URI = Uri.parse("content://" + LOGINS_AUTHORITY);
public static final String PARAM_PROFILE = "profile";
public static final String PARAM_PROFILE_PATH = "profilePath";
public static final String PARAM_LIMIT = "limit";
public static final String PARAM_SUGGESTEDSITES_LIMIT = "suggestedsites_limit";
public static final String PARAM_TOPSITES_DISABLE_PINNED = "topsites_disable_pinned";
public static final String PARAM_IS_SYNC = "sync";
public static final String PARAM_SHOW_DELETED = "show_deleted";
public static final String PARAM_IS_TEST = "test";
public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
public static final String PARAM_INCREMENT_VISITS = "increment_visits";
public static final String PARAM_INCREMENT_REMOTE_AGGREGATES = "increment_remote_aggregates";
public static final String PARAM_EXPIRE_PRIORITY = "priority";
public static final String PARAM_DATASET_ID = "dataset_id";
public static final String PARAM_GROUP_BY = "group_by";
static public enum ExpirePriority {
NORMAL,
AGGRESSIVE
}
/**
* Produces a SQL expression used for sorting results of the "combined" view by frecency.
* Combines remote and local frecency calculations, weighting local visits much heavier.
*
* @param includesBookmarks When URL is bookmarked, should we give it bonus frecency points?
* @param ascending Indicates if sorting order ascending
* @return Combined frecency sorting expression
*/
static public String getCombinedFrecencySortOrder(boolean includesBookmarks, boolean ascending) {
final long now = System.currentTimeMillis();
StringBuilder order = new StringBuilder(getRemoteFrecencySQL(now) + " + " + getLocalFrecencySQL(now));
if (includesBookmarks) {
order.insert(0, "(CASE WHEN " + Combined.BOOKMARK_ID + " > -1 THEN 100 ELSE 0 END) + ");
}
order.append(ascending ? " ASC" : " DESC");
return order.toString();
}
/**
* See Bug 1265525 for details (explanation + graphs) on how Remote frecency compares to Local frecency for different
* combinations of visits count and age.
*
* @param now Base time in milliseconds for age calculation
* @return remote frecency SQL calculation
*/
static public String getRemoteFrecencySQL(final long now) {
return getFrecencyCalculation(now, 1, 110, Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_DATE_LAST_VISITED);
}
/**
* Local frecency SQL calculation. Note higher scale factor and squared visit count which achieve
* visits generated locally being much preferred over remote visits.
* See Bug 1265525 for details (explanation + comparison graphs).
*
* @param now Base time in milliseconds for age calculation
* @return local frecency SQL calculation
*/
static public String getLocalFrecencySQL(final long now) {
String visitCountExpr = "(" + Combined.LOCAL_VISITS_COUNT + " + 2)";
visitCountExpr = visitCountExpr + " * " + visitCountExpr;
return getFrecencyCalculation(now, 2, 225, visitCountExpr, Combined.LOCAL_DATE_LAST_VISITED);
}
/**
* Our version of frecency is computed by scaling the number of visits by a multiplier
* that approximates Gaussian decay, based on how long ago the entry was last visited.
* Since we're limited by the math we can do with sqlite, we're calculating this
* approximation using the Cauchy distribution: multiplier = scale_const / (age^2 + scale_const).
* For example, with 15 as our scale parameter, we get a scale constant 15^2 = 225. Then:
* frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977)
*
* @param now Base time in milliseconds for age calculation
* @param minFrecency Minimum allowed frecency value
* @param multiplier Scale constant
* @param visitCountExpr Expression which will produce a visit count
* @param lastVisitExpr Expression which will produce "last-visited" timestamp
* @return Frecency SQL calculation
*/
static public String getFrecencyCalculation(final long now, final int minFrecency, final int multiplier, @NonNull final String visitCountExpr, @NonNull final String lastVisitExpr) {
final long nowInMicroseconds = now * 1000;
final long microsecondsPerDay = 86400000000L;
final String ageExpr = "(" + nowInMicroseconds + " - " + lastVisitExpr + ") / " + microsecondsPerDay;
return visitCountExpr + " * MAX(" + minFrecency + ", 100 * " + multiplier + " / (" + ageExpr + " * " + ageExpr + " + " + multiplier + "))";
}
@RobocopTarget
public interface CommonColumns {
public static final String _ID = "_id";
}
@RobocopTarget
public interface DateSyncColumns {
public static final String DATE_CREATED = "created";
public static final String DATE_MODIFIED = "modified";
}
@RobocopTarget
public interface SyncColumns extends DateSyncColumns {
public static final String GUID = "guid";
public static final String IS_DELETED = "deleted";
}
@RobocopTarget
public interface URLColumns {
public static final String URL = "url";
public static final String TITLE = "title";
}
@RobocopTarget
public interface FaviconColumns {
public static final String FAVICON = "favicon";
public static final String FAVICON_ID = "favicon_id";
public static final String FAVICON_URL = "favicon_url";
}
@RobocopTarget
public interface HistoryColumns {
public static final String DATE_LAST_VISITED = "date";
public static final String VISITS = "visits";
// Aggregates used to speed up top sites and search frecency-powered queries
public static final String LOCAL_VISITS = "visits_local";
public static final String REMOTE_VISITS = "visits_remote";
public static final String LOCAL_DATE_LAST_VISITED = "date_local";
public static final String REMOTE_DATE_LAST_VISITED = "date_remote";
}
@RobocopTarget
public interface VisitsColumns {
public static final String HISTORY_GUID = "history_guid";
public static final String VISIT_TYPE = "visit_type";
public static final String DATE_VISITED = "date";
// Used to distinguish between visits that were generated locally vs those that came in from Sync.
// Since we don't track "origin clientID" for visits, this is the best we can do for now.
public static final String IS_LOCAL = "is_local";
}
public interface PageMetadataColumns {
public static final String HISTORY_GUID = "history_guid";
public static final String DATE_CREATED = "created";
public static final String HAS_IMAGE = "has_image";
public static final String JSON = "json";
}
public interface DeletedColumns {
public static final String ID = "id";
public static final String GUID = "guid";
public static final String TIME_DELETED = "timeDeleted";
}
@RobocopTarget
public static final class Favicons implements CommonColumns, DateSyncColumns {
private Favicons() {}
public static final String TABLE_NAME = "favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "favicons");
public static final String URL = "url";
public static final String DATA = "data";
public static final String PAGE_URL = "page_url";
}
@RobocopTarget
public static final class Thumbnails implements CommonColumns {
private Thumbnails() {}
public static final String TABLE_NAME = "thumbnails";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "thumbnails");
public static final String URL = "url";
public static final String DATA = "data";
}
public static final class Profiles {
private Profiles() {}
public static final String NAME = "name";
public static final String PATH = "path";
}
@RobocopTarget
public static final class Bookmarks implements CommonColumns, URLColumns, FaviconColumns, SyncColumns {
private Bookmarks() {}
public static final String TABLE_NAME = "bookmarks";
public static final String VIEW_WITH_FAVICONS = "bookmarks_with_favicons";
public static final String VIEW_WITH_ANNOTATIONS = "bookmarks_with_annotations";
public static final int FIXED_ROOT_ID = 0;
public static final int FAKE_DESKTOP_FOLDER_ID = -1;
public static final int FIXED_READING_LIST_ID = -2;
public static final int FIXED_PINNED_LIST_ID = -3;
public static final int FIXED_SCREENSHOT_FOLDER_ID = -4;
public static final int FAKE_READINGLIST_SMARTFOLDER_ID = -5;
/**
* This ID and the following negative IDs are reserved for bookmarks from Android's partner
* bookmark provider.
*/
public static final long FAKE_PARTNER_BOOKMARKS_START = -1000;
public static final String MOBILE_FOLDER_GUID = "mobile";
public static final String PLACES_FOLDER_GUID = "places";
public static final String MENU_FOLDER_GUID = "menu";
public static final String TAGS_FOLDER_GUID = "tags";
public static final String TOOLBAR_FOLDER_GUID = "toolbar";
public static final String UNFILED_FOLDER_GUID = "unfiled";
public static final String FAKE_DESKTOP_FOLDER_GUID = "desktop";
public static final String PINNED_FOLDER_GUID = "pinned";
public static final String SCREENSHOT_FOLDER_GUID = "screenshots";
public static final String FAKE_READINGLIST_SMARTFOLDER_GUID = "readinglist";
public static final int TYPE_FOLDER = 0;
public static final int TYPE_BOOKMARK = 1;
public static final int TYPE_SEPARATOR = 2;
public static final int TYPE_LIVEMARK = 3;
public static final int TYPE_QUERY = 4;
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks");
public static final Uri PARENTS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "parents");
// Hacky API for bulk-updating positions. Bug 728783.
public static final Uri POSITIONS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "positions");
public static final long DEFAULT_POSITION = Long.MIN_VALUE;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/bookmark";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/bookmark";
public static final String TYPE = "type";
public static final String PARENT = "parent";
public static final String POSITION = "position";
public static final String TAGS = "tags";
public static final String DESCRIPTION = "description";
public static final String KEYWORD = "keyword";
public static final String ANNOTATION_KEY = "annotation_key";
public static final String ANNOTATION_VALUE = "annotation_value";
}
@RobocopTarget
public static final class History implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns, SyncColumns {
private History() {}
public static final String TABLE_NAME = "history";
public static final String VIEW_WITH_FAVICONS = "history_with_favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history");
public static final Uri CONTENT_OLD_URI = Uri.withAppendedPath(AUTHORITY_URI, "history/old");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history";
}
@RobocopTarget
public static final class Visits implements CommonColumns, VisitsColumns {
private Visits() {}
public static final String TABLE_NAME = "visits";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "visits");
public static final int VISIT_IS_LOCAL = 1;
public static final int VISIT_IS_REMOTE = 0;
}
// Combined bookmarks and history
@RobocopTarget
public static final class Combined implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns {
private Combined() {}
public static final String VIEW_NAME = "combined";
public static final String VIEW_WITH_FAVICONS = "combined_with_favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined");
public static final String BOOKMARK_ID = "bookmark_id";
public static final String HISTORY_ID = "history_id";
public static final String REMOTE_VISITS_COUNT = "remoteVisitCount";
public static final String REMOTE_DATE_LAST_VISITED = "remoteDateLastVisited";
public static final String LOCAL_VISITS_COUNT = "localVisitCount";
public static final String LOCAL_DATE_LAST_VISITED = "localDateLastVisited";
}
public static final class Schema {
private Schema() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "schema");
public static final String VERSION = "version";
}
public static final class Passwords {
private Passwords() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "passwords");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/passwords";
public static final String ID = "id";
public static final String HOSTNAME = "hostname";
public static final String HTTP_REALM = "httpRealm";
public static final String FORM_SUBMIT_URL = "formSubmitURL";
public static final String USERNAME_FIELD = "usernameField";
public static final String PASSWORD_FIELD = "passwordField";
public static final String ENCRYPTED_USERNAME = "encryptedUsername";
public static final String ENCRYPTED_PASSWORD = "encryptedPassword";
public static final String ENC_TYPE = "encType";
public static final String TIME_CREATED = "timeCreated";
public static final String TIME_LAST_USED = "timeLastUsed";
public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged";
public static final String TIMES_USED = "timesUsed";
public static final String GUID = "guid";
// This needs to be kept in sync with the types defined in toolkit/components/passwordmgr/nsILoginManagerCrypto.idl#45
public static final int ENCTYPE_SDR = 1;
}
public static final class DeletedPasswords implements DeletedColumns {
private DeletedPasswords() {}
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-passwords";
public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "deleted-passwords");
}
@RobocopTarget
public static final class GeckoDisabledHosts {
private GeckoDisabledHosts() {}
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/disabled-hosts";
public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "disabled-hosts");
public static final String HOSTNAME = "hostname";
}
public static final class FormHistory {
private FormHistory() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "formhistory");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/formhistory";
public static final String ID = "id";
public static final String FIELD_NAME = "fieldname";
public static final String VALUE = "value";
public static final String TIMES_USED = "timesUsed";
public static final String FIRST_USED = "firstUsed";
public static final String LAST_USED = "lastUsed";
public static final String GUID = "guid";
}
public static final class DeletedFormHistory implements DeletedColumns {
private DeletedFormHistory() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "deleted-formhistory");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-formhistory";
}
@RobocopTarget
public static final class Tabs implements CommonColumns {
private Tabs() {}
public static final String TABLE_NAME = "tabs";
public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "tabs");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/tab";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/tab";
// Title of the tab.
public static final String TITLE = "title";
// Topmost URL from the history array. Allows processing of this tab without
// parsing that array.
public static final String URL = "url";
// Sync-assigned GUID for client device. NULL for local tabs.
public static final String CLIENT_GUID = "client_guid";
// JSON-encoded array of history URL strings, from most recent to least recent.
public static final String HISTORY = "history";
// Favicon URL for the tab's topmost history entry.
public static final String FAVICON = "favicon";
// Last used time of the tab.
public static final String LAST_USED = "last_used";
// Position of the tab. 0 represents foreground.
public static final String POSITION = "position";
}
public static final class Clients implements CommonColumns {
private Clients() {}
public static final Uri CONTENT_RECENCY_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients_recency");
public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/client";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/client";
// Client-provided name string. Could conceivably be null.
public static final String NAME = "name";
// Sync-assigned GUID for client device. NULL for local tabs.
public static final String GUID = "guid";
// Last modified time for the client's tab record. For remote records, a server
// timestamp provided by Sync during insertion.
public static final String LAST_MODIFIED = "last_modified";
public static final String DEVICE_TYPE = "device_type";
}
// Data storage for dynamic panels on about:home
@RobocopTarget
public static final class HomeItems implements CommonColumns {
private HomeItems() {}
public static final Uri CONTENT_FAKE_URI = Uri.withAppendedPath(HOME_AUTHORITY_URI, "items/fake");
public static final Uri CONTENT_URI = Uri.withAppendedPath(HOME_AUTHORITY_URI, "items");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/homeitem";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/homeitem";
public static final String DATASET_ID = "dataset_id";
public static final String URL = "url";
public static final String TITLE = "title";
public static final String DESCRIPTION = "description";
public static final String IMAGE_URL = "image_url";
public static final String BACKGROUND_COLOR = "background_color";
public static final String BACKGROUND_URL = "background_url";
public static final String CREATED = "created";
public static final String FILTER = "filter";
public static final String[] DEFAULT_PROJECTION =
new String[] { _ID, DATASET_ID, URL, TITLE, DESCRIPTION, IMAGE_URL, BACKGROUND_COLOR, BACKGROUND_URL, FILTER };
}
@RobocopTarget
public static final class ReadingListItems implements CommonColumns, URLColumns {
public static final String EXCERPT = "excerpt";
public static final String CLIENT_LAST_MODIFIED = "client_last_modified";
public static final String GUID = "guid";
public static final String SERVER_LAST_MODIFIED = "last_modified";
public static final String SERVER_STORED_ON = "stored_on";
public static final String ADDED_ON = "added_on";
public static final String MARKED_READ_ON = "marked_read_on";
public static final String IS_DELETED = "is_deleted";
public static final String IS_ARCHIVED = "is_archived";
public static final String IS_UNREAD = "is_unread";
public static final String IS_ARTICLE = "is_article";
public static final String IS_FAVORITE = "is_favorite";
public static final String RESOLVED_URL = "resolved_url";
public static final String RESOLVED_TITLE = "resolved_title";
public static final String ADDED_BY = "added_by";
public static final String MARKED_READ_BY = "marked_read_by";
public static final String WORD_COUNT = "word_count";
public static final String READ_POSITION = "read_position";
public static final String CONTENT_STATUS = "content_status";
public static final String SYNC_STATUS = "sync_status";
public static final String SYNC_CHANGE_FLAGS = "sync_change_flags";
private ReadingListItems() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(READING_LIST_AUTHORITY_URI, "items");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/readinglistitem";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/readinglistitem";
// CONTENT_STATUS represents the result of an attempt to fetch content for the reading list item.
public static final int STATUS_UNFETCHED = 0;
public static final int STATUS_FETCH_FAILED_TEMPORARY = 1;
public static final int STATUS_FETCH_FAILED_PERMANENT = 2;
public static final int STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT = 3;
public static final int STATUS_FETCHED_ARTICLE = 4;
// See https://github.com/mozilla-services/readinglist/wiki/Client-phases for how this is expected to work.
//
// If an item is SYNCED, it doesn't need to be uploaded.
//
// If its status is NEW, the entire record should be uploaded.
//
// If DELETED, the record should be deleted. A record can only move into this state from SYNCED; NEW records
// are deleted immediately.
//
public static final int SYNC_STATUS_SYNCED = 0;
public static final int SYNC_STATUS_NEW = 1; // Upload everything.
public static final int SYNC_STATUS_DELETED = 2; // Delete the record from the server.
public static final int SYNC_STATUS_MODIFIED = 3; // Consult SYNC_CHANGE_FLAGS.
// SYNC_CHANGE_FLAG represents the sets of fields that need to be uploaded.
// If its status is only UNREAD_CHANGED (and maybe FAVORITE_CHANGED?), then it can easily be uploaded
// in a fire-and-forget manner. This change can never conflict.
//
// If its status is RESOLVED, then one or more of the content-oriented fields has changed, and a full
// upload of those fields should occur. These can result in conflicts.
//
// Note that these are flags; they should be considered together when deciding on a course of action.
//
// These flags are meaningless for records in any state other than SYNCED. They can be safely altered in
// other states (to avoid having to query to pre-fill a ContentValues), but should be ignored.
public static final int SYNC_CHANGE_NONE = 0;
public static final int SYNC_CHANGE_UNREAD_CHANGED = 1 << 0; // => marked_read_{on,by}, is_unread
public static final int SYNC_CHANGE_FAVORITE_CHANGED = 1 << 1; // => is_favorite
public static final int SYNC_CHANGE_RESOLVED = 1 << 2; // => is_article, resolved_{url,title}, excerpt, word_count
public static final String DEFAULT_SORT_ORDER = CLIENT_LAST_MODIFIED + " DESC";
public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, WORD_COUNT, IS_UNREAD };
// Minimum fields required to create a reading list item.
public static final String[] REQUIRED_FIELDS = { ReadingListItems.URL, ReadingListItems.TITLE };
// All fields that might be mapped from the DB into a record object.
public static final String[] ALL_FIELDS = {
CommonColumns._ID,
URLColumns.URL,
URLColumns.TITLE,
EXCERPT,
CLIENT_LAST_MODIFIED,
GUID,
SERVER_LAST_MODIFIED,
SERVER_STORED_ON,
ADDED_ON,
MARKED_READ_ON,
IS_DELETED,
IS_ARCHIVED,
IS_UNREAD,
IS_ARTICLE,
IS_FAVORITE,
RESOLVED_URL,
RESOLVED_TITLE,
ADDED_BY,
MARKED_READ_BY,
WORD_COUNT,
READ_POSITION,
CONTENT_STATUS,
SYNC_STATUS,
SYNC_CHANGE_FLAGS,
};
public static final String TABLE_NAME = "reading_list";
}
@RobocopTarget
public static final class TopSites implements CommonColumns, URLColumns {
private TopSites() {}
public static final int TYPE_BLANK = 0;
public static final int TYPE_TOP = 1;
public static final int TYPE_PINNED = 2;
public static final int TYPE_SUGGESTED = 3;
public static final String BOOKMARK_ID = "bookmark_id";
public static final String HISTORY_ID = "history_id";
public static final String TYPE = "type";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "topsites");
}
public static final class Highlights {
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "highlights");
public static final String DATE = "date";
}
@RobocopTarget
public static final class SearchHistory implements CommonColumns, HistoryColumns {
private SearchHistory() {}
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searchhistory";
public static final String QUERY = "query";
public static final String DATE = "date";
public static final String TABLE_NAME = "searchhistory";
public static final Uri CONTENT_URI = Uri.withAppendedPath(SEARCH_HISTORY_AUTHORITY_URI, "searchhistory");
}
@RobocopTarget
public static final class SuggestedSites implements CommonColumns, URLColumns {
private SuggestedSites() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "suggestedsites");
}
public static final class ActivityStreamBlocklist implements CommonColumns {
private ActivityStreamBlocklist() {}
public static final String TABLE_NAME = "activity_stream_blocklist";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME);
public static final String URL = "url";
public static final String CREATED = "created";
}
@RobocopTarget
public static final class UrlAnnotations implements CommonColumns, DateSyncColumns {
private UrlAnnotations() {}
public static final String TABLE_NAME = "urlannotations";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME);
public static final String URL = "url";
public static final String KEY = "key";
public static final String VALUE = "value";
public static final String SYNC_STATUS = "sync_status";
public enum Key {
// We use a parameter, rather than name(), as defensive coding: we can't let the
// enum name change because we've already stored values into the DB.
SCREENSHOT ("screenshot"),
/**
* This key maps URLs to its feeds.
*
* Key: feed
* Value: URL of feed
*/
FEED("feed"),
/**
* This key maps URLs of feeds to an object describing the feed.
*
* Key: feed_subscription
* Value: JSON object describing feed
*/
FEED_SUBSCRIPTION("feed_subscription"),
/**
* Indicates that this URL (if stored as a bookmark) should be opened into reader view.
*
* Key: reader_view
* Value: String "true" to indicate that we would like to open into reader view.
*/
READER_VIEW("reader_view"),
/**
* Indicator that the user interacted with the URL in regards to home screen shortcuts.
*
* Key: home_screen_shortcut
* Value: True: User created an home screen shortcut for this URL
* False: User declined to create a shortcut for this URL
*/
HOME_SCREEN_SHORTCUT("home_screen_shortcut");
private final String dbValue;
Key(final String dbValue) { this.dbValue = dbValue; }
public String getDbValue() { return dbValue; }
}
public enum SyncStatus {
// We use a parameter, rather than ordinal(), as defensive coding: we can't let the
// ordinal values change because we've already stored values into the DB.
NEW (0);
// Value stored into the database for this column.
private final int dbValue;
SyncStatus(final int dbValue) {
this.dbValue = dbValue;
}
public int getDBValue() { return dbValue; }
}
/**
* Value used to indicate that a reader view item is saved. We use the
*/
public static final String READER_VIEW_SAVED_VALUE = "true";
}
public static final class Numbers {
private Numbers() {}
public static final String TABLE_NAME = "numbers";
public static final String POSITION = "position";
public static final int MAX_VALUE = 50;
}
@RobocopTarget
public static final class Logins implements CommonColumns {
private Logins() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "logins");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/logins";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/logins";
public static final String TABLE_LOGINS = "logins";
public static final String HOSTNAME = "hostname";
public static final String HTTP_REALM = "httpRealm";
public static final String FORM_SUBMIT_URL = "formSubmitURL";
public static final String USERNAME_FIELD = "usernameField";
public static final String PASSWORD_FIELD = "passwordField";
public static final String ENCRYPTED_USERNAME = "encryptedUsername";
public static final String ENCRYPTED_PASSWORD = "encryptedPassword";
public static final String ENC_TYPE = "encType";
public static final String TIME_CREATED = "timeCreated";
public static final String TIME_LAST_USED = "timeLastUsed";
public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged";
public static final String TIMES_USED = "timesUsed";
public static final String GUID = "guid";
}
@RobocopTarget
public static final class DeletedLogins implements CommonColumns {
private DeletedLogins() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "deleted-logins");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-logins";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/deleted-logins";
public static final String TABLE_DELETED_LOGINS = "deleted_logins";
public static final String GUID = "guid";
public static final String TIME_DELETED = "timeDeleted";
}
@RobocopTarget
public static final class LoginsDisabledHosts implements CommonColumns {
private LoginsDisabledHosts() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "logins-disabled-hosts");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/logins-disabled-hosts";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/logins-disabled-hosts";
public static final String TABLE_DISABLED_HOSTS = "logins_disabled_hosts";
public static final String HOSTNAME = "hostname";
}
@RobocopTarget
public static final class PageMetadata implements CommonColumns, PageMetadataColumns {
private PageMetadata() {}
public static final String TABLE_NAME = "page_metadata";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "page_metadata");
}
// We refer to the service by name to decouple services from the rest of the code base.
public static final String TAB_RECEIVED_SERVICE_CLASS_NAME = "org.mozilla.gecko.tabqueue.TabReceivedService";
public static final String SKIP_TAB_QUEUE_FLAG = "skip_tab_queue";
public static final String EXTRA_CLIENT_GUID = "org.mozilla.gecko.extra.CLIENT_ID";
}

View file

@ -0,0 +1,205 @@
/* 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.db;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.drawable.BitmapDrawable;
import android.support.v4.content.CursorLoader;
/**
* Interface for interactions with all databases. If you want an instance
* that implements this, you should go through GeckoProfile. E.g.,
* <code>BrowserDB.from(context)</code>.
*/
public abstract class BrowserDB {
public static enum FilterFlags {
EXCLUDE_PINNED_SITES
}
public abstract Searches getSearches();
public abstract TabsAccessor getTabsAccessor();
public abstract URLMetadata getURLMetadata();
@RobocopTarget public abstract UrlAnnotations getUrlAnnotations();
/**
* Add default bookmarks to the database.
* Takes an offset; returns a new offset.
*/
public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset);
/**
* Add bookmarks from the provided distribution.
* Takes an offset; returns a new offset.
*/
public abstract int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset);
/**
* Invalidate cached data.
*/
public abstract void invalidate();
public abstract int getCount(ContentResolver cr, String database);
/**
* @return a cursor representing the contents of the DB filtered according to the arguments.
* Can return <code>null</code>. <code>CursorLoader</code> will handle this correctly.
*/
public abstract Cursor filter(ContentResolver cr, CharSequence constraint,
int limit, EnumSet<BrowserDB.FilterFlags> flags);
/**
* @return a cursor over top sites (high-ranking bookmarks and history).
* Can return <code>null</code>.
* Returns no more than <code>limit</code> results.
* Suggested sites will be limited to being within the first <code>suggestedRangeLimit</code> results.
*/
public abstract Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit);
public abstract CursorLoader getActivityStreamTopSites(Context context, int limit);
public abstract void updateVisitedHistory(ContentResolver cr, String uri);
public abstract void updateHistoryTitle(ContentResolver cr, String uri, String title);
/**
* Can return <code>null</code>.
*/
public abstract Cursor getAllVisitedHistory(ContentResolver cr);
/**
* Can return <code>null</code>.
*/
public abstract Cursor getRecentHistory(ContentResolver cr, int limit);
public abstract Cursor getHistoryForURL(ContentResolver cr, String uri);
public abstract Cursor getRecentHistoryBetweenTime(ContentResolver cr, int historyLimit, long start, long end);
public abstract long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath);
public abstract void expireHistory(ContentResolver cr, ExpirePriority priority);
public abstract void removeHistoryEntry(ContentResolver cr, String url);
public abstract void clearHistory(ContentResolver cr, boolean clearSearchHistory);
public abstract String getUrlForKeyword(ContentResolver cr, String keyword);
public abstract boolean isBookmark(ContentResolver cr, String uri);
public abstract boolean addBookmark(ContentResolver cr, String title, String uri);
public abstract Cursor getBookmarkForUrl(ContentResolver cr, String url);
public abstract Cursor getBookmarksForPartialUrl(ContentResolver cr, String partialUrl);
public abstract void removeBookmarksWithURL(ContentResolver cr, String uri);
public abstract void registerBookmarkObserver(ContentResolver cr, ContentObserver observer);
public abstract void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword);
public abstract boolean hasBookmarkWithGuid(ContentResolver cr, String guid);
public abstract boolean insertPageMetadata(ContentProviderClient contentProviderClient, String pageUrl, boolean hasImage, String metadataJSON);
public abstract int deletePageMetadata(ContentProviderClient contentProviderClient, String pageUrl);
/**
* Can return <code>null</code>.
*/
public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
public abstract int getBookmarkCountForFolder(ContentResolver cr, long folderId);
/**
* Get the favicon from the database, if any, associated with the given favicon URL. (That is,
* the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
* @param cr The ContentResolver to use.
* @param faviconURL The URL of the favicon to fetch from the database.
* @return The decoded Bitmap from the database, if any. null if none is stored.
*/
public abstract LoadFaviconResult getFaviconForUrl(Context context, ContentResolver cr, String faviconURL);
/**
* Try to find a usable favicon URL in the history or bookmarks table.
*/
public abstract String getFaviconURLFromPageURL(ContentResolver cr, String uri);
public abstract byte[] getThumbnailForUrl(ContentResolver cr, String uri);
public abstract void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail);
/**
* Query for non-null thumbnails matching the provided <code>urls</code>.
* The returned cursor will have no more than, but possibly fewer than,
* the requested number of thumbnails.
*
* Returns null if the provided list of URLs is empty or null.
*/
public abstract Cursor getThumbnailsForUrls(ContentResolver cr,
List<String> urls);
public abstract void removeThumbnails(ContentResolver cr);
// Utility function for updating existing history using batch operations
public abstract void updateHistoryInBatch(ContentResolver cr,
Collection<ContentProviderOperation> operations, String url,
String title, long date, int visits);
public abstract void updateBookmarkInBatch(ContentResolver cr,
Collection<ContentProviderOperation> operations, String url,
String title, String guid, long parent, long added, long modified,
long position, String keyword, int type);
public abstract void pinSite(ContentResolver cr, String url, String title, int position);
public abstract void unpinSite(ContentResolver cr, int position);
public abstract boolean hideSuggestedSite(String url);
public abstract void setSuggestedSites(SuggestedSites suggestedSites);
public abstract SuggestedSites getSuggestedSites();
public abstract boolean hasSuggestedImageUrl(String url);
public abstract String getSuggestedImageUrlForUrl(String url);
public abstract int getSuggestedBackgroundColorForUrl(String url);
/**
* Obtain a set of links for highlights from bookmarks and history.
*
* @param context The context to load the cursor.
* @param limit Maximum number of results to return.
*/
public abstract CursorLoader getHighlights(Context context, int limit);
/**
* Block a page from the highlights list.
*
* @param url The page URL. Only pages exactly matching this URL will be blocked.
*/
public abstract void blockActivityStreamSite(ContentResolver cr, String url);
public static BrowserDB from(final Context context) {
return from(GeckoProfile.get(context));
}
public static BrowserDB from(final GeckoProfile profile) {
synchronized (profile.getLock()) {
BrowserDB db = (BrowserDB) profile.getData();
if (db != null) {
return db;
}
db = new LocalBrowserDB(profile.getName());
profile.setData(db);
return db;
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,450 @@
/* 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.db;
import android.annotation.TargetApi;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.os.Build;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoProfile;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.Telemetry;
import java.util.Map;
public class DBUtils {
private static final String LOGTAG = "GeckoDBUtils";
public static final int SQLITE_MAX_VARIABLE_NUMBER = 999;
public static final String qualifyColumn(String table, String column) {
return table + "." + column;
}
// This is available in Android >= 11. Implemented locally to be
// compatible with older versions.
public static String concatenateWhere(String a, String b) {
if (TextUtils.isEmpty(a)) {
return b;
}
if (TextUtils.isEmpty(b)) {
return a;
}
return "(" + a + ") AND (" + b + ")";
}
// This is available in Android >= 11. Implemented locally to be
// compatible with older versions.
public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
if (originalValues == null || originalValues.length == 0) {
return newValues;
}
if (newValues == null || newValues.length == 0) {
return originalValues;
}
String[] result = new String[originalValues.length + newValues.length];
System.arraycopy(originalValues, 0, result, 0, originalValues.length);
System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
return result;
}
/**
* Concatenate multiple lists of selection arguments. <code>values</code> may be <code>null</code>.
*/
public static String[] concatenateSelectionArgs(String[]... values) {
// Since we're most likely to be concatenating a few arrays of many values, it is most
// efficient to iterate over the arrays once to obtain their lengths, allowing us to create one target array
// (as opposed to copying arrays on every iteration, which would result in many more copies).
int totalLength = 0;
for (String[] v : values) {
if (v != null) {
totalLength += v.length;
}
}
String[] result = new String[totalLength];
int position = 0;
for (String[] v: values) {
if (v != null) {
int currentLength = v.length;
System.arraycopy(v, 0, result, position, currentLength);
position += currentLength;
}
}
return result;
}
public static void replaceKey(ContentValues aValues, String aOriginalKey,
String aNewKey, String aDefault) {
String value = aDefault;
if (aOriginalKey != null && aValues.containsKey(aOriginalKey)) {
value = aValues.get(aOriginalKey).toString();
aValues.remove(aOriginalKey);
}
if (!aValues.containsKey(aNewKey)) {
aValues.put(aNewKey, value);
}
}
private static String HISTOGRAM_DATABASE_LOCKED = "DATABASE_LOCKED_EXCEPTION";
private static String HISTOGRAM_DATABASE_UNLOCKED = "DATABASE_SUCCESSFUL_UNLOCK";
public static void ensureDatabaseIsNotLocked(SQLiteOpenHelper dbHelper, String databasePath) {
final int maxAttempts = 5;
int attempt = 0;
SQLiteDatabase db = null;
for (; attempt < maxAttempts; attempt++) {
try {
// Try a simple test and exit the loop.
db = dbHelper.getWritableDatabase();
break;
} catch (Exception e) {
// We assume that this is a android.database.sqlite.SQLiteDatabaseLockedException.
// That class is only available on API 11+.
Telemetry.addToHistogram(HISTOGRAM_DATABASE_LOCKED, attempt);
// Things could get very bad if we don't find a way to unlock the DB.
Log.d(LOGTAG, "Database is locked, trying to kill any zombie processes: " + databasePath);
GeckoAppShell.killAnyZombies();
try {
Thread.sleep(attempt * 100);
} catch (InterruptedException ie) {
}
}
}
if (db == null) {
Log.w(LOGTAG, "Failed to unlock database.");
GeckoAppShell.listOfOpenFiles();
return;
}
// If we needed to retry, but we succeeded, report that in telemetry.
// Failures are indicated by a lower frequency of UNLOCKED than LOCKED.
if (attempt > 1) {
Telemetry.addToHistogram(HISTOGRAM_DATABASE_UNLOCKED, attempt - 1);
}
}
/**
* Copies a table <b>between</b> database files.
*
* This method assumes that the source table and destination table already exist in the
* source and destination databases, respectively.
*
* The table is copied row-by-row in a single transaction.
*
* @param source The source database that the table will be copied from.
* @param sourceTableName The name of the source table.
* @param destination The destination database that the table will be copied to.
* @param destinationTableName The name of the destination table.
* @return true if all rows were copied; false otherwise.
*/
public static boolean copyTable(SQLiteDatabase source, String sourceTableName,
SQLiteDatabase destination, String destinationTableName) {
Cursor cursor = null;
try {
destination.beginTransaction();
cursor = source.query(sourceTableName, null, null, null, null, null, null);
Log.d(LOGTAG, "Trying to copy " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName);
final ContentValues contentValues = new ContentValues();
while (cursor.moveToNext()) {
contentValues.clear();
DatabaseUtils.cursorRowToContentValues(cursor, contentValues);
destination.insert(destinationTableName, null, contentValues);
}
destination.setTransactionSuccessful();
Log.d(LOGTAG, "Successfully copied " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName);
return true;
} catch (Exception e) {
Log.w(LOGTAG, "Got exception copying rows from " + sourceTableName + " to " + destinationTableName + "; ignoring.", e);
return false;
} finally {
destination.endTransaction();
if (cursor != null) {
cursor.close();
}
}
}
/**
* Verifies that 0-byte arrays aren't added as favicon or thumbnail data.
* @param values ContentValues of query
* @param columnName Name of data column to verify
*/
public static void stripEmptyByteArray(ContentValues values, String columnName) {
if (values.containsKey(columnName)) {
byte[] data = values.getAsByteArray(columnName);
if (data == null || data.length == 0) {
Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring.");
values.putNull(columnName);
}
}
}
/**
* Builds a selection string that searches for a list of arguments in a particular column.
* For example URL in (?,?,?). Callers should pass the actual arguments into their query
* as selection args.
* @para columnName The column to search in
* @para size The number of arguments to search for
*/
public static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
/**
* Turn a single-column cursor of longs into a single SQL "IN" clause.
* We can do this without using selection arguments because Long isn't
* vulnerable to injection.
*/
public static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
final int commaLimit = cursor.getCount() - 1;
int i = 0;
while (cursor.moveToNext()) {
builder.append(cursor.getLong(0));
if (i++ < commaLimit) {
builder.append(", ");
}
}
builder.append(")");
return builder.toString();
}
public static Uri appendProfile(final String profile, final Uri uri) {
return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, profile).build();
}
public static Uri appendProfileWithDefault(final String profile, final Uri uri) {
if (profile == null) {
return appendProfile(GeckoProfile.DEFAULT_PROFILE, uri);
}
return appendProfile(profile, uri);
}
/**
* Use the following when no conflict action is specified.
*/
private static final int CONFLICT_NONE = 0;
private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
/**
* Convenience method for updating rows in the database.
*
* @param table the table to update in
* @param values a map from column names to new column values. null is a
* valid value that will be translated to NULL.
* @param whereClause the optional WHERE clause to apply when updating.
* Passing null will update all rows.
* @param whereArgs You may include ?s in the where clause, which
* will be replaced by the values from whereArgs. The values
* will be bound as Strings.
* @return the number of rows affected
*/
@RobocopTarget
public static int updateArrays(SQLiteDatabase db, String table, ContentValues[] values, UpdateOperation[] ops, String whereClause, String[] whereArgs) {
return updateArraysWithOnConflict(db, table, values, ops, whereClause, whereArgs, CONFLICT_NONE, true);
}
public static void updateArraysBlindly(SQLiteDatabase db, String table, ContentValues[] values, UpdateOperation[] ops, String whereClause, String[] whereArgs) {
updateArraysWithOnConflict(db, table, values, ops, whereClause, whereArgs, CONFLICT_NONE, false);
}
@RobocopTarget
public enum UpdateOperation {
/**
* ASSIGN is the usual update: replaces the value in the named column with the provided value.
*
* foo = ?
*/
ASSIGN,
/**
* BITWISE_OR applies the provided value to the existing value with a bitwise OR. This is useful for adding to flags.
*
* foo |= ?
*/
BITWISE_OR,
/**
* EXPRESSION is an end-run around the API: it allows callers to specify a fragment of SQL to splice into the
* SET part of the query.
*
* foo = $value
*
* Be very careful not to use user input in this.
*/
EXPRESSION,
}
/**
* This is an evil reimplementation of SQLiteDatabase's methods to allow for
* smarter updating.
*
* Each ContentValues has an associated enum that describes how to unify input values with the existing column values.
*/
private static int updateArraysWithOnConflict(SQLiteDatabase db, String table,
ContentValues[] values,
UpdateOperation[] ops,
String whereClause,
String[] whereArgs,
int conflictAlgorithm,
boolean returnChangedRows) {
if (values == null || values.length == 0) {
throw new IllegalArgumentException("Empty values");
}
if (ops == null || ops.length != values.length) {
throw new IllegalArgumentException("ops and values don't match");
}
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(CONFLICT_VALUES[conflictAlgorithm]);
sql.append(table);
sql.append(" SET ");
// move all bind args to one array
int setValuesSize = 0;
for (int i = 0; i < values.length; i++) {
// EXPRESSION types don't contribute any placeholders.
if (ops[i] != UpdateOperation.EXPRESSION) {
setValuesSize += values[i].size();
}
}
int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
Object[] bindArgs = new Object[bindArgsSize];
int arg = 0;
for (int i = 0; i < values.length; i++) {
final ContentValues v = values[i];
final UpdateOperation op = ops[i];
// Alas, code duplication.
switch (op) {
case ASSIGN:
for (Map.Entry<String, Object> entry : v.valueSet()) {
final String colName = entry.getKey();
sql.append((arg > 0) ? "," : "");
sql.append(colName);
bindArgs[arg++] = entry.getValue();
sql.append("= ?");
}
break;
case BITWISE_OR:
for (Map.Entry<String, Object> entry : v.valueSet()) {
final String colName = entry.getKey();
sql.append((arg > 0) ? "," : "");
sql.append(colName);
bindArgs[arg++] = entry.getValue();
sql.append("= ? | ");
sql.append(colName);
}
break;
case EXPRESSION:
// Treat each value as a literal SQL string.
for (Map.Entry<String, Object> entry : v.valueSet()) {
final String colName = entry.getKey();
sql.append((arg > 0) ? "," : "");
sql.append(colName);
sql.append(" = ");
sql.append(entry.getValue());
}
break;
}
}
if (whereArgs != null) {
for (arg = setValuesSize; arg < bindArgsSize; arg++) {
bindArgs[arg] = whereArgs[arg - setValuesSize];
}
}
if (!TextUtils.isEmpty(whereClause)) {
sql.append(" WHERE ");
sql.append(whereClause);
}
// What a huge pain in the ass, all because SQLiteDatabase doesn't expose .executeSql,
// and we can't get a DB handle. Nor can we easily construct a statement with arguments
// already bound.
final SQLiteStatement statement = db.compileStatement(sql.toString());
try {
bindAllArgs(statement, bindArgs);
if (!returnChangedRows) {
statement.execute();
return 0;
}
// This is a separate method so we can annotate it with @TargetApi.
return executeStatementReturningChangedRows(statement);
} finally {
statement.close();
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static int executeStatementReturningChangedRows(SQLiteStatement statement) {
return statement.executeUpdateDelete();
}
// All because {@link SQLiteProgram#bind(integer, Object)} is private.
private static void bindAllArgs(SQLiteStatement statement, Object[] bindArgs) {
if (bindArgs == null) {
return;
}
for (int i = bindArgs.length; i != 0; i--) {
Object v = bindArgs[i - 1];
if (v == null) {
statement.bindNull(i);
} else if (v instanceof String) {
statement.bindString(i, (String) v);
} else if (v instanceof Double) {
statement.bindDouble(i, (Double) v);
} else if (v instanceof Float) {
statement.bindDouble(i, (Float) v);
} else if (v instanceof Long) {
statement.bindLong(i, (Long) v);
} else if (v instanceof Integer) {
statement.bindLong(i, (Integer) v);
} else if (v instanceof Byte) {
statement.bindLong(i, (Byte) v);
} else if (v instanceof byte[]) {
statement.bindBlob(i, (byte[]) v);
}
}
}
}

View file

@ -0,0 +1,166 @@
/* 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.db;
import java.lang.IllegalArgumentException;
import java.util.HashMap;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.db.BrowserContract.FormHistory;
import org.mozilla.gecko.db.BrowserContract.DeletedFormHistory;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sqlite.SQLiteBridge;
import org.mozilla.gecko.sync.Utils;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
public class FormHistoryProvider extends SQLiteBridgeContentProvider {
static final String TABLE_FORM_HISTORY = "moz_formhistory";
static final String TABLE_DELETED_FORM_HISTORY = "moz_deleted_formhistory";
private static final int FORM_HISTORY = 100;
private static final int DELETED_FORM_HISTORY = 101;
private static final UriMatcher URI_MATCHER;
// This should be kept in sync with the db version in toolkit/components/satchel/nsFormHistory.js
private static final int DB_VERSION = 4;
private static final String DB_FILENAME = "formhistory.sqlite";
private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_FORMS";
private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedFormHistory.GUID + " IS NULL";
private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedFormHistory.GUID + " = ?";
private static final String LOG_TAG = "FormHistoryProvider";
static {
URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "formhistory", FORM_HISTORY);
URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "deleted-formhistory", DELETED_FORM_HISTORY);
}
public FormHistoryProvider() {
super(LOG_TAG);
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case FORM_HISTORY:
return FormHistory.CONTENT_TYPE;
case DELETED_FORM_HISTORY:
return DeletedFormHistory.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown type " + uri);
}
}
@Override
public String getTable(Uri uri) {
String table = null;
final int match = URI_MATCHER.match(uri);
switch (match) {
case DELETED_FORM_HISTORY:
table = TABLE_DELETED_FORM_HISTORY;
break;
case FORM_HISTORY:
table = TABLE_FORM_HISTORY;
break;
default:
throw new UnsupportedOperationException("Unknown table " + uri);
}
return table;
}
@Override
public String getSortOrder(Uri uri, String aRequested) {
if (!TextUtils.isEmpty(aRequested)) {
return aRequested;
}
return null;
}
@Override
public void setupDefaults(Uri uri, ContentValues values) {
int match = URI_MATCHER.match(uri);
long now = System.currentTimeMillis();
switch (match) {
case DELETED_FORM_HISTORY:
values.put(DeletedFormHistory.TIME_DELETED, now);
// Deleted entries must contain a guid
if (!values.containsKey(FormHistory.GUID)) {
throw new IllegalArgumentException("Must provide a GUID for a deleted form history");
}
break;
case FORM_HISTORY:
// Generate GUID for new entry. Don't override specified GUIDs.
if (!values.containsKey(FormHistory.GUID)) {
String guid = Utils.generateGuid();
values.put(FormHistory.GUID, guid);
}
break;
default:
throw new UnsupportedOperationException("Unknown insert URI " + uri);
}
}
@Override
public void initGecko() {
GeckoAppShell.notifyObservers("FormHistory:Init", null);
}
@Override
public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
if (!values.containsKey(FormHistory.GUID)) {
return;
}
String guid = values.getAsString(FormHistory.GUID);
if (guid == null) {
db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_NULL, null);
return;
}
String[] args = new String[] { guid };
db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_VALUE, args);
}
@Override
public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { }
@Override
public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { }
@Override
protected String getDBName() {
return DB_FILENAME;
}
@Override
protected String getTelemetryPrefix() {
return TELEMETRY_TAG;
}
@Override
protected int getDBVersion() {
return DB_VERSION;
}
}

View file

@ -0,0 +1,194 @@
/* 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.db;
import java.io.IOException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.HomeItems;
import org.mozilla.gecko.db.DBUtils;
import org.mozilla.gecko.sqlite.SQLiteBridge;
import org.mozilla.gecko.util.RawResource;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.Log;
public class HomeProvider extends SQLiteBridgeContentProvider {
private static final String LOGTAG = "GeckoHomeProvider";
// This should be kept in sync with the db version in mobile/android/modules/HomeProvider.jsm
private static final int DB_VERSION = 3;
private static final String DB_FILENAME = "home.sqlite";
private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_HOME";
private static final String TABLE_ITEMS = "items";
// Endpoint to return static fake data.
static final int ITEMS_FAKE = 100;
static final int ITEMS = 101;
static final int ITEMS_ID = 102;
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static {
URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items/fake", ITEMS_FAKE);
URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items", ITEMS);
URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items/#", ITEMS_ID);
}
public HomeProvider() {
super(LOGTAG);
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS_FAKE: {
return HomeItems.CONTENT_TYPE;
}
case ITEMS: {
return HomeItems.CONTENT_TYPE;
}
default: {
throw new UnsupportedOperationException("Unknown type " + uri);
}
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final int match = URI_MATCHER.match(uri);
// If we're querying the fake items, don't try to get the database.
if (match == ITEMS_FAKE) {
return queryFakeItems(uri, projection, selection, selectionArgs, sortOrder);
}
final String datasetId = uri.getQueryParameter(BrowserContract.PARAM_DATASET_ID);
if (datasetId == null) {
throw new IllegalArgumentException("All queries should contain a dataset ID parameter");
}
selection = DBUtils.concatenateWhere(selection, HomeItems.DATASET_ID + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { datasetId });
// Otherwise, let the SQLiteContentProvider implementation take care of this query for us!
Cursor c = super.query(uri, projection, selection, selectionArgs, sortOrder);
// SQLiteBridgeContentProvider may return a null Cursor if the database hasn't been created yet.
// However, we need a non-null cursor in order to listen for notifications.
if (c == null) {
c = new MatrixCursor(projection != null ? projection : HomeItems.DEFAULT_PROJECTION);
}
final ContentResolver cr = getContext().getContentResolver();
c.setNotificationUri(cr, getDatasetNotificationUri(datasetId));
return c;
}
/**
* Returns a cursor populated with static fake data.
*/
private Cursor queryFakeItems(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
JSONArray items = null;
try {
final String jsonString = RawResource.getAsString(getContext(), R.raw.fake_home_items);
items = new JSONArray(jsonString);
} catch (IOException e) {
Log.e(LOGTAG, "Error getting fake home items", e);
return null;
} catch (JSONException e) {
Log.e(LOGTAG, "Error parsing fake_home_items.json", e);
return null;
}
final MatrixCursor c = new MatrixCursor(HomeItems.DEFAULT_PROJECTION);
for (int i = 0; i < items.length(); i++) {
try {
final JSONObject item = items.getJSONObject(i);
c.addRow(new Object[] {
item.getInt("id"),
item.getString("dataset_id"),
item.getString("url"),
item.getString("title"),
item.getString("description"),
item.getString("image_url"),
item.getString("filter")
});
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating cursor row for fake home item", e);
}
}
return c;
}
/**
* SQLiteBridgeContentProvider implementation
*/
@Override
protected String getDBName() {
return DB_FILENAME;
}
@Override
protected String getTelemetryPrefix() {
return TELEMETRY_TAG;
}
@Override
protected int getDBVersion() {
return DB_VERSION;
}
@Override
public String getTable(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS: {
return TABLE_ITEMS;
}
default: {
throw new UnsupportedOperationException("Unknown table " + uri);
}
}
}
@Override
public String getSortOrder(Uri uri, String aRequested) {
return null;
}
@Override
public void setupDefaults(Uri uri, ContentValues values) { }
@Override
public void initGecko() { }
@Override
public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) { }
@Override
public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { }
@Override
public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { }
public static Uri getDatasetNotificationUri(String datasetId) {
return Uri.withAppendedPath(HomeItems.CONTENT_URI, datasetId);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
/* -*- 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.db;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
/**
* Helper class for dealing with the search provider inside Fennec.
*/
public class LocalSearches implements Searches {
private final Uri uriWithProfile;
public LocalSearches(String mProfile) {
uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.SearchHistory.CONTENT_URI);
}
@Override
public void insert(ContentResolver cr, String query) {
final ContentValues values = new ContentValues();
values.put(BrowserContract.SearchHistory.QUERY, query);
cr.insert(uriWithProfile, values);
}
}

View file

@ -0,0 +1,320 @@
/* 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.db;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class LocalTabsAccessor implements TabsAccessor {
private static final String LOGTAG = "GeckoTabsAccessor";
private static final long THREE_WEEKS_IN_MILLISECONDS = TimeUnit.MILLISECONDS.convert(21L, TimeUnit.DAYS);
public static final String[] TABS_PROJECTION_COLUMNS = new String[] {
BrowserContract.Tabs.TITLE,
BrowserContract.Tabs.URL,
BrowserContract.Clients.GUID,
BrowserContract.Clients.NAME,
BrowserContract.Tabs.LAST_USED,
BrowserContract.Clients.LAST_MODIFIED,
BrowserContract.Clients.DEVICE_TYPE,
};
public static final String[] CLIENTS_PROJECTION_COLUMNS = new String[] {
BrowserContract.Clients.GUID,
BrowserContract.Clients.NAME,
BrowserContract.Clients.LAST_MODIFIED,
BrowserContract.Clients.DEVICE_TYPE
};
private static final String REMOTE_CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL";
private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
private static final String REMOTE_TABS_SELECTION_CLIENT_RECENCY = REMOTE_TABS_SELECTION +
" AND " + BrowserContract.Clients.LAST_MODIFIED + " > ?";
private static final String REMOTE_TABS_SORT_ORDER =
// Most recently synced clients first.
BrowserContract.Clients.LAST_MODIFIED + " DESC, " +
// If two clients somehow had the same last modified time, this will
// group them (arbitrarily).
BrowserContract.Clients.GUID + " DESC, " +
// Within a single client, most recently used tabs first.
BrowserContract.Tabs.LAST_USED + " DESC";
private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL";
private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");
private final Uri clientsRecencyUriWithProfile;
private final Uri tabsUriWithProfile;
private final Uri clientsUriWithProfile;
public LocalTabsAccessor(String profileName) {
tabsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Tabs.CONTENT_URI);
clientsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_URI);
clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI);
}
/**
* Extracts a List of just RemoteClients from a cursor.
* The supplied cursor should be grouped by guid and sorted by most recently used.
*/
@Override
public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
final ArrayList<RemoteClient> clients = new ArrayList<>(cursor.getCount());
final int originalPosition = cursor.getPosition();
try {
if (!cursor.moveToFirst()) {
return clients;
}
final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
while (!cursor.isAfterLast()) {
final String clientGuid = cursor.getString(clientGuidIndex);
final String clientName = cursor.getString(clientNameIndex);
final String deviceType = cursor.getString(clientDeviceTypeIndex);
final long lastModified = cursor.getLong(clientLastModifiedIndex);
clients.add(new RemoteClient(clientGuid, clientName, lastModified, deviceType));
cursor.moveToNext();
}
} finally {
cursor.moveToPosition(originalPosition);
}
return clients;
}
/**
* Extract client and tab records from a cursor.
* <p>
* The position of the cursor is moved to before the first record before
* reading. The cursor is advanced until there are no more records to be
* read. The position of the cursor is restored before returning.
*
* @param cursor
* to extract records from. The records should already be grouped
* by client GUID.
* @return list of clients, each containing list of tabs.
*/
@Override
public List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
final ArrayList<RemoteClient> clients = new ArrayList<RemoteClient>();
final int originalPosition = cursor.getPosition();
try {
if (!cursor.moveToFirst()) {
return clients;
}
final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL);
final int tabLastUsedIndex = cursor.getColumnIndex(BrowserContract.Tabs.LAST_USED);
final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
// A walking partition, chunking by client GUID. We assume the
// cursor records are already grouped by client GUID; see the query
// sort order.
RemoteClient lastClient = null;
while (!cursor.isAfterLast()) {
final String clientGuid = cursor.getString(clientGuidIndex);
if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) {
final String clientName = cursor.getString(clientNameIndex);
final long lastModified = cursor.getLong(clientLastModifiedIndex);
final String deviceType = cursor.getString(clientDeviceTypeIndex);
lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType);
clients.add(lastClient);
}
final String tabTitle = cursor.getString(tabTitleIndex);
final String tabUrl = cursor.getString(tabUrlIndex);
final long tabLastUsed = cursor.getLong(tabLastUsedIndex);
lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl, tabLastUsed));
cursor.moveToNext();
}
} finally {
cursor.moveToPosition(originalPosition);
}
return clients;
}
@Override
public Cursor getRemoteClientsByRecencyCursor(Context context) {
final Uri uri = clientsRecencyUriWithProfile;
return context.getContentResolver().query(uri, CLIENTS_PROJECTION_COLUMNS,
REMOTE_CLIENTS_SELECTION, null, null);
}
@Override
public Cursor getRemoteTabsCursor(Context context) {
return getRemoteTabsCursor(context, -1);
}
@Override
public Cursor getRemoteTabsCursor(Context context, int limit) {
Uri uri = tabsUriWithProfile;
if (limit > 0) {
uri = uri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
.build();
}
final String threeWeeksAgoTimestampMillis = Long.valueOf(
System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS).toString();
return context.getContentResolver().query(uri,
TABS_PROJECTION_COLUMNS,
REMOTE_TABS_SELECTION_CLIENT_RECENCY,
new String[] {threeWeeksAgoTimestampMillis},
REMOTE_TABS_SORT_ORDER);
}
// This method returns all tabs from all remote clients,
// ordered by most recent client first, most recent tab first
@Override
public void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
getTabs(context, 0, listener);
}
// This method returns limited number of tabs from all remote clients,
// ordered by most recent client first, most recent tab first
@Override
public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
// If there is no listener, no point in doing work.
if (listener == null)
return;
(new UIAsyncTask.WithoutParams<List<RemoteClient>>(ThreadUtils.getBackgroundHandler()) {
@Override
protected List<RemoteClient> doInBackground() {
final Cursor cursor = getRemoteTabsCursor(context, limit);
if (cursor == null)
return null;
try {
return Collections.unmodifiableList(getClientsFromCursor(cursor));
} finally {
cursor.close();
}
}
@Override
protected void onPostExecute(List<RemoteClient> clients) {
listener.onQueryTabsComplete(clients);
}
}).execute();
}
// Updates the modified time of the local client with the current time.
private void updateLocalClient(final ContentResolver cr) {
ContentValues values = new ContentValues();
values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
cr.update(clientsUriWithProfile, values, LOCAL_CLIENT_SELECTION, null);
}
// Deletes all local tabs.
private void deleteLocalTabs(final ContentResolver cr) {
cr.delete(tabsUriWithProfile, LOCAL_TABS_SELECTION, null);
}
/**
* Tabs are positioned in the DB in the same order that they appear in the tabs param.
* - URL should never empty or null. Skip this tab if there's no URL.
* - TITLE should always a string, either a page title or empty.
* - LAST_USED should always be numeric.
* - FAVICON should be a URL or null.
* - HISTORY should be serialized JSON array of URLs.
* - POSITION should always be numeric.
* - CLIENT_GUID should always be null to represent the local client.
*/
private void insertLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
// Reuse this for serializing individual history URLs as JSON.
JSONArray history = new JSONArray();
ArrayList<ContentValues> valuesToInsert = new ArrayList<ContentValues>();
int position = 0;
for (Tab tab : tabs) {
// Skip this tab if it has a null URL or is in private browsing mode, or is a filtered URL.
String url = tab.getURL();
if (url == null || tab.isPrivate() || isFilteredURL(url))
continue;
ContentValues values = new ContentValues();
values.put(BrowserContract.Tabs.URL, url);
values.put(BrowserContract.Tabs.TITLE, tab.getTitle());
values.put(BrowserContract.Tabs.LAST_USED, tab.getLastUsed());
String favicon = tab.getFaviconURL();
if (favicon != null)
values.put(BrowserContract.Tabs.FAVICON, favicon);
else
values.putNull(BrowserContract.Tabs.FAVICON);
// We don't have access to session history in Java, so for now, we'll
// just use a JSONArray that holds most recent history item.
try {
history.put(0, tab.getURL());
values.put(BrowserContract.Tabs.HISTORY, history.toString());
} catch (JSONException e) {
Log.w(LOGTAG, "JSONException adding URL to tab history array.", e);
}
values.put(BrowserContract.Tabs.POSITION, position++);
// A null client guid corresponds to the local client.
values.putNull(BrowserContract.Tabs.CLIENT_GUID);
valuesToInsert.add(values);
}
ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]);
cr.bulkInsert(tabsUriWithProfile, valuesToInsertArray);
}
// Deletes all local tabs and replaces them with a new list of tabs.
@Override
public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
deleteLocalTabs(cr);
insertLocalTabs(cr, tabs);
updateLocalClient(cr);
}
/**
* Matches the supplied URL string against the set of URLs to filter.
*
* @return true if the supplied URL should be skipped; false otherwise.
*/
private boolean isFilteredURL(String url) {
return FILTERED_URL_PATTERN.matcher(url).lookingAt();
}
}

View file

@ -0,0 +1,240 @@
/* -*- 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.db;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import android.util.LruCache;
// Holds metadata info about URLs. Supports some helper functions for getting back a HashMap of key value data.
public class LocalURLMetadata implements URLMetadata {
private static final String LOGTAG = "GeckoURLMetadata";
private final Uri uriWithProfile;
public LocalURLMetadata(String mProfile) {
uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, URLMetadataTable.CONTENT_URI);
}
// A list of columns in the table. It's used to simplify some loops for reading/writing data.
private static final Set<String> COLUMNS;
static {
final HashSet<String> tempModel = new HashSet<>(4);
tempModel.add(URLMetadataTable.URL_COLUMN);
tempModel.add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
tempModel.add(URLMetadataTable.TILE_COLOR_COLUMN);
tempModel.add(URLMetadataTable.TOUCH_ICON_COLUMN);
COLUMNS = Collections.unmodifiableSet(tempModel);
}
// Store a cache of recent results. This number is chosen to match the max number of tiles on about:home
private static final int CACHE_SIZE = 9;
// Note: Members of this cache are unmodifiable.
private final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);
/**
* Converts a JSON object into a unmodifiable Map of known metadata properties.
* Will throw away any properties that aren't stored in the database.
*
* Incoming data can include a list like: {touchIconList:{56:"http://x.com/56.png", 76:"http://x.com/76.png"}}.
* This will then be filtered to find the most appropriate touchIcon, i.e. the closest icon size that is larger
* than (or equal to) the preferred homescreen launcher icon size, which is then stored in the "touchIcon" property.
*/
@Override
public Map<String, Object> fromJSON(JSONObject obj) {
Map<String, Object> data = new HashMap<String, Object>();
for (String key : COLUMNS) {
if (obj.has(key)) {
data.put(key, obj.optString(key));
}
}
try {
JSONObject icons;
if (obj.has("touchIconList") &&
(icons = obj.getJSONObject("touchIconList")).length() > 0) {
int preferredSize = GeckoAppShell.getPreferredIconSize();
Iterator<String> keys = icons.keys();
ArrayList<Integer> sizes = new ArrayList<Integer>(icons.length());
while (keys.hasNext()) {
sizes.add(new Integer(keys.next()));
}
final int bestSize = LoadFaviconResult.selectBestSizeFromList(sizes, preferredSize);
final String iconURL = icons.getString(Integer.toString(bestSize));
data.put(URLMetadataTable.TOUCH_ICON_COLUMN, iconURL);
}
} catch (JSONException e) {
Log.w(LOGTAG, "Exception processing touchIconList for LocalURLMetadata; ignoring.", e);
}
return Collections.unmodifiableMap(data);
}
/**
* Converts a Cursor into a unmodifiable Map of known metadata properties.
* Will throw away any properties that aren't stored in the database.
* Will also not iterate through multiple rows in the cursor.
*/
private Map<String, Object> fromCursor(Cursor c) {
Map<String, Object> data = new HashMap<String, Object>();
String[] columns = c.getColumnNames();
for (String column : columns) {
if (COLUMNS.contains(column)) {
try {
data.put(column, c.getString(c.getColumnIndexOrThrow(column)));
} catch (Exception ex) {
Log.i(LOGTAG, "Error getting data for " + column, ex);
}
}
}
return Collections.unmodifiableMap(data);
}
/**
* Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls.
* Must not be called from UI or Gecko threads.
*/
@Override
public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
final Collection<String> urls,
final List<String> requestedColumns) {
ThreadUtils.assertNotOnUiThread();
ThreadUtils.assertNotOnGeckoThread();
final Map<String, Map<String, Object>> data = new HashMap<String, Map<String, Object>>();
// Nothing to query for
if (urls.isEmpty() || requestedColumns.isEmpty()) {
Log.e(LOGTAG, "Queried metadata for nothing");
return data;
}
// Search the cache for any of these urls
List<String> urlsToQuery = new ArrayList<String>();
for (String url : urls) {
final Map<String, Object> hit = cache.get(url);
if (hit != null) {
// Cache hit: we've found the URL in the cache, however we may not have cached the desired columns
// for that URL. Hence we need to check whether our cache hit contains those columns, and directly
// retrieve the desired data if not. (E.g. the top sites panel retrieves the tile, and tilecolor. If
// we later try to retrieve the touchIcon for a top-site the cache hit will only point to
// tile+tilecolor, and not the required touchIcon. In this case we don't want to use the cache.)
boolean useCache = true;
for (String c: requestedColumns) {
if (!hit.containsKey(c)) {
useCache = false;
}
}
if (useCache) {
data.put(url, hit);
} else {
urlsToQuery.add(url);
}
} else {
urlsToQuery.add(url);
}
}
// If everything was in the cache, we're done!
if (urlsToQuery.size() == 0) {
return Collections.unmodifiableMap(data);
}
final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN);
List<String> columns = requestedColumns;
// We need the url to build our final HashMap, so we force it to be included in the query.
if (!columns.contains(URLMetadataTable.URL_COLUMN)) {
// The requestedColumns may be immutable (e.g. if the caller used Collections.singletonList), hence
// we have to create a copy.
columns = new ArrayList<String>(columns);
columns.add(URLMetadataTable.URL_COLUMN);
}
final Cursor cursor = cr.query(uriWithProfile,
columns.toArray(new String[columns.size()]), // columns,
selection, // selection
urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs
null);
try {
if (!cursor.moveToFirst()) {
return Collections.unmodifiableMap(data);
}
do {
final Map<String, Object> metadata = fromCursor(cursor);
final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN));
data.put(url, metadata);
cache.put(url, metadata);
} while (cursor.moveToNext());
} finally {
cursor.close();
}
return Collections.unmodifiableMap(data);
}
/**
* Saves a HashMap of metadata into the database. Will iterate through columns
* in the Database and only save rows with matching keys in the HashMap.
* Must not be called from UI or Gecko threads.
*/
@Override
public void save(final ContentResolver cr, final Map<String, Object> data) {
ThreadUtils.assertNotOnUiThread();
ThreadUtils.assertNotOnGeckoThread();
try {
ContentValues values = new ContentValues();
for (String key : COLUMNS) {
if (data.containsKey(key)) {
values.put(key, (String) data.get(key));
}
}
if (values.size() == 0) {
return;
}
Uri uri = uriWithProfile.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
.build();
cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] {
(String) data.get(URLMetadataTable.URL_COLUMN)
});
} catch (Exception ex) {
Log.e(LOGTAG, "error saving", ex);
}
}
}

View file

@ -0,0 +1,253 @@
/* 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.db;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.json.JSONException;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.db.BrowserContract.UrlAnnotations.Key;
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
public class LocalUrlAnnotations implements UrlAnnotations {
private static final String LOGTAG = "LocalUrlAnnotations";
private Uri urlAnnotationsTableWithProfile;
public LocalUrlAnnotations(final String profile) {
urlAnnotationsTableWithProfile = DBUtils.appendProfile(profile, BrowserContract.UrlAnnotations.CONTENT_URI);
}
/**
* Get all feed subscriptions.
*/
@Override
public Cursor getFeedSubscriptions(ContentResolver cr) {
return queryByKey(cr,
Key.FEED_SUBSCRIPTION,
new String[] { BrowserContract.UrlAnnotations.URL, BrowserContract.UrlAnnotations.VALUE },
null);
}
/**
* Insert mapping from website URL to URL of the feed.
*/
@Override
public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {
insertAnnotation(cr, originUrl, Key.FEED, feedUrl);
}
@Override
public boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url) {
return hasResultsForSelection(cr,
BrowserContract.UrlAnnotations.URL + " = ?",
new String[]{url});
}
@Override
public void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut) {
insertAnnotation(cr, url, Key.HOME_SCREEN_SHORTCUT, String.valueOf(hasCreatedShortCut));
}
/**
* Returns true if there's a mapping from the given website URL to a feed URL. False otherwise.
*/
@Override
public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) {
return hasResultsForSelection(cr,
BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
new String[]{websiteUrl, Key.FEED.getDbValue()});
}
/**
* Returns true if there's a website URL with this feed URL. False otherwise.
*/
@Override
public boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl) {
return hasResultsForSelection(cr,
BrowserContract.UrlAnnotations.VALUE + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
new String[]{feedUrl, Key.FEED.getDbValue()});
}
/**
* Delete the feed URL mapping for this website URL.
*/
@Override
public void deleteFeedUrl(ContentResolver cr, String websiteUrl) {
deleteAnnotation(cr, websiteUrl, Key.FEED);
}
/**
* Get website URLs that are mapped to the given feed URL.
*/
@Override
public Cursor getWebsitesWithFeedUrl(ContentResolver cr) {
return cr.query(urlAnnotationsTableWithProfile,
new String[] { BrowserContract.UrlAnnotations.URL },
BrowserContract.UrlAnnotations.KEY + " = ?",
new String[] { Key.FEED.getDbValue() },
null);
}
/**
* Returns true if there's a subscription for this feed URL. False otherwise.
*/
@Override
public boolean hasFeedSubscription(ContentResolver cr, String feedUrl) {
return hasResultsForSelection(cr,
BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
new String[]{feedUrl, Key.FEED_SUBSCRIPTION.getDbValue()});
}
/**
* Insert the given feed subscription (Mapping from feed URL to the subscription object).
*/
@Override
public void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
try {
insertAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString());
} catch (JSONException e) {
Log.w(LOGTAG, "Could not serialize subscription");
}
}
/**
* Update the feed subscription with new values.
*/
@Override
public void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
try {
updateAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString());
} catch (JSONException e) {
Log.w(LOGTAG, "Could not serialize subscription");
}
}
/**
* Delete the subscription for the feed URL.
*/
@Override
public void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
deleteAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION);
}
private int deleteAnnotation(final ContentResolver cr, final String url, final Key key) {
return cr.delete(urlAnnotationsTableWithProfile,
BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?",
new String[] { key.getDbValue(), url });
}
private int updateAnnotation(final ContentResolver cr, final String url, final Key key, final String value) {
ContentValues values = new ContentValues();
values.put(BrowserContract.UrlAnnotations.VALUE, value);
values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, System.currentTimeMillis());
return cr.update(urlAnnotationsTableWithProfile,
values,
BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?",
new String[]{key.getDbValue(), url});
}
private void insertAnnotation(final ContentResolver cr, final String url, final Key key, final String value) {
insertAnnotation(cr, url, key.getDbValue(), value);
}
@RobocopTarget
@Override
public void insertAnnotation(final ContentResolver cr, final String url, final String key, final String value) {
final long creationTime = System.currentTimeMillis();
final ContentValues values = new ContentValues(5);
values.put(BrowserContract.UrlAnnotations.URL, url);
values.put(BrowserContract.UrlAnnotations.KEY, key);
values.put(BrowserContract.UrlAnnotations.VALUE, value);
values.put(BrowserContract.UrlAnnotations.DATE_CREATED, creationTime);
values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, creationTime);
cr.insert(urlAnnotationsTableWithProfile, values);
}
/**
* @return true if the table contains rows for the given selection.
*/
private boolean hasResultsForSelection(ContentResolver cr, String selection, String[] selectionArgs) {
Cursor cursor = cr.query(urlAnnotationsTableWithProfile,
new String[] { BrowserContract.UrlAnnotations._ID },
selection,
selectionArgs,
null);
if (cursor == null) {
return false;
}
try {
return cursor.getCount() > 0;
} finally {
cursor.close();
}
}
private Cursor queryByKey(final ContentResolver cr, @NonNull final Key key, @Nullable final String[] projections,
@Nullable final String sortOrder) {
return cr.query(urlAnnotationsTableWithProfile,
projections,
BrowserContract.UrlAnnotations.KEY + " = ?", new String[] { key.getDbValue() },
sortOrder);
}
@Override
public Cursor getScreenshots(ContentResolver cr) {
return queryByKey(cr,
Key.SCREENSHOT,
new String[] {
BrowserContract.UrlAnnotations._ID,
BrowserContract.UrlAnnotations.URL,
BrowserContract.UrlAnnotations.KEY,
BrowserContract.UrlAnnotations.VALUE,
BrowserContract.UrlAnnotations.DATE_CREATED,
},
BrowserContract.UrlAnnotations.DATE_CREATED + " DESC");
}
public void insertScreenshot(final ContentResolver cr, final String pageUrl, final String screenshotPath) {
insertAnnotation(cr, pageUrl, Key.SCREENSHOT.getDbValue(), screenshotPath);
}
@Override
public void insertReaderViewUrl(final ContentResolver cr, final String pageUrl) {
insertAnnotation(cr, pageUrl, Key.READER_VIEW.getDbValue(), BrowserContract.UrlAnnotations.READER_VIEW_SAVED_VALUE);
}
@Override
public void deleteReaderViewUrl(ContentResolver cr, String pageURL) {
deleteAnnotation(cr, pageURL, Key.READER_VIEW);
}
public int getAnnotationCount(ContentResolver cr, Key key) {
final String countColumnname = "count";
final Cursor c = queryByKey(cr,
key,
new String[] {
"COUNT(*) AS " + countColumnname
},
null);
try {
if (c != null && c.moveToFirst()) {
return c.getInt(c.getColumnIndexOrThrow(countColumnname));
} else {
return 0;
}
} finally {
if (c != null) {
c.close();
}
}
}
}

View file

@ -0,0 +1,520 @@
/* 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.db;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Base64;
import org.mozilla.gecko.db.BrowserContract.DeletedLogins;
import org.mozilla.gecko.db.BrowserContract.Logins;
import org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts;
import org.mozilla.gecko.sync.Utils;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import javax.crypto.Cipher;
import javax.crypto.NullCipher;
import static org.mozilla.gecko.db.BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS;
import static org.mozilla.gecko.db.BrowserContract.Logins.TABLE_LOGINS;
import static org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS;
public class LoginsProvider extends SharedBrowserDatabaseProvider {
private static final int LOGINS = 100;
private static final int LOGINS_ID = 101;
private static final int DELETED_LOGINS = 102;
private static final int DELETED_LOGINS_ID = 103;
private static final int DISABLED_HOSTS = 104;
private static final int DISABLED_HOSTS_HOSTNAME = 105;
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
private static final HashMap<String, String> LOGIN_PROJECTION_MAP;
private static final HashMap<String, String> DELETED_LOGIN_PROJECTION_MAP;
private static final HashMap<String, String> DISABLED_HOSTS_PROJECTION_MAP;
private static final String DEFAULT_LOGINS_SORT_ORDER = Logins.HOSTNAME + " ASC";
private static final String DEFAULT_DELETED_LOGINS_SORT_ORDER = DeletedLogins.TIME_DELETED + " ASC";
private static final String DEFAULT_DISABLED_HOSTS_SORT_ORDER = LoginsDisabledHosts.HOSTNAME + " ASC";
private static final String WHERE_GUID_IS_NULL = DeletedLogins.GUID + " IS NULL";
private static final String WHERE_GUID_IS_VALUE = DeletedLogins.GUID + " = ?";
protected static final String INDEX_LOGINS_HOSTNAME = "login_hostname_index";
protected static final String INDEX_LOGINS_HOSTNAME_FORM_SUBMIT_URL = "login_hostname_formSubmitURL_index";
protected static final String INDEX_LOGINS_HOSTNAME_HTTP_REALM = "login_hostname_httpRealm_index";
static {
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins", LOGINS);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins/#", LOGINS_ID);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "deleted-logins", DELETED_LOGINS);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "deleted-logins/#", DELETED_LOGINS_ID);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins-disabled-hosts", DISABLED_HOSTS);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins-disabled-hosts/hostname/*", DISABLED_HOSTS_HOSTNAME);
LOGIN_PROJECTION_MAP = new HashMap<>();
LOGIN_PROJECTION_MAP.put(Logins._ID, Logins._ID);
LOGIN_PROJECTION_MAP.put(Logins.HOSTNAME, Logins.HOSTNAME);
LOGIN_PROJECTION_MAP.put(Logins.HTTP_REALM, Logins.HTTP_REALM);
LOGIN_PROJECTION_MAP.put(Logins.FORM_SUBMIT_URL, Logins.FORM_SUBMIT_URL);
LOGIN_PROJECTION_MAP.put(Logins.USERNAME_FIELD, Logins.USERNAME_FIELD);
LOGIN_PROJECTION_MAP.put(Logins.PASSWORD_FIELD, Logins.PASSWORD_FIELD);
LOGIN_PROJECTION_MAP.put(Logins.ENCRYPTED_USERNAME, Logins.ENCRYPTED_USERNAME);
LOGIN_PROJECTION_MAP.put(Logins.ENCRYPTED_PASSWORD, Logins.ENCRYPTED_PASSWORD);
LOGIN_PROJECTION_MAP.put(Logins.GUID, Logins.GUID);
LOGIN_PROJECTION_MAP.put(Logins.ENC_TYPE, Logins.ENC_TYPE);
LOGIN_PROJECTION_MAP.put(Logins.TIME_CREATED, Logins.TIME_CREATED);
LOGIN_PROJECTION_MAP.put(Logins.TIME_LAST_USED, Logins.TIME_LAST_USED);
LOGIN_PROJECTION_MAP.put(Logins.TIME_PASSWORD_CHANGED, Logins.TIME_PASSWORD_CHANGED);
LOGIN_PROJECTION_MAP.put(Logins.TIMES_USED, Logins.TIMES_USED);
DELETED_LOGIN_PROJECTION_MAP = new HashMap<>();
DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins._ID, DeletedLogins._ID);
DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins.GUID, DeletedLogins.GUID);
DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins.TIME_DELETED, DeletedLogins.TIME_DELETED);
DISABLED_HOSTS_PROJECTION_MAP = new HashMap<>();
DISABLED_HOSTS_PROJECTION_MAP.put(LoginsDisabledHosts._ID, LoginsDisabledHosts._ID);
DISABLED_HOSTS_PROJECTION_MAP.put(LoginsDisabledHosts.HOSTNAME, LoginsDisabledHosts.HOSTNAME);
}
private static String projectColumn(String table, String column) {
return table + "." + column;
}
private static String selectColumn(String table, String column) {
return projectColumn(table, column) + " = ?";
}
@Override
protected Uri insertInTransaction(Uri uri, ContentValues values) {
trace("Calling insert in transaction on URI: " + uri);
final int match = URI_MATCHER.match(uri);
final SQLiteDatabase db = getWritableDatabase(uri);
final long id;
String guid;
setupDefaultValues(values, uri);
switch (match) {
case LOGINS:
removeDeletedLoginsByGUIDInTransaction(values, db);
// Encrypt sensitive data.
encryptContentValueFields(values);
guid = values.getAsString(Logins.GUID);
debug("Inserting login in database with GUID: " + guid);
id = db.insertOrThrow(TABLE_LOGINS, Logins.GUID, values);
break;
case DELETED_LOGINS:
guid = values.getAsString(DeletedLogins.GUID);
debug("Inserting deleted-login in database with GUID: " + guid);
id = db.insertOrThrow(TABLE_DELETED_LOGINS, DeletedLogins.GUID, values);
break;
case DISABLED_HOSTS:
String hostname = values.getAsString(LoginsDisabledHosts.HOSTNAME);
debug("Inserting disabled-host in database with hostname: " + hostname);
id = db.insertOrThrow(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME, values);
break;
default:
throw new UnsupportedOperationException("Unknown insert URI " + uri);
}
debug("Inserted ID in database: " + id);
if (id >= 0) {
return ContentUris.withAppendedId(uri, id);
}
return null;
}
@Override
@SuppressWarnings("fallthrough")
protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete in transaction on URI: " + uri);
final int match = URI_MATCHER.match(uri);
final String table;
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
switch (match) {
case LOGINS_ID:
trace("Delete on LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[]{Long.toString(ContentUris.parseId(uri))});
// Store the deleted client in deleted-logins table.
final String guid = getLoginGUIDByID(selection, selectionArgs, db);
if (guid == null) {
// No matching logins found for the id.
return 0;
}
boolean isInsertSuccessful = storeDeletedLoginForGUIDInTransaction(guid, db);
if (!isInsertSuccessful) {
// Failed to insert into deleted-logins, return early.
return 0;
}
// fall through
case LOGINS:
trace("Delete on LOGINS: " + uri);
table = TABLE_LOGINS;
break;
case DELETED_LOGINS_ID:
trace("Delete on DELETED_LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DELETED_LOGINS, DeletedLogins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[]{Long.toString(ContentUris.parseId(uri))});
// fall through
case DELETED_LOGINS:
trace("Delete on DELETED_LOGINS_ID: " + uri);
table = TABLE_DELETED_LOGINS;
break;
case DISABLED_HOSTS_HOSTNAME:
trace("Delete on DISABLED_HOSTS_HOSTNAME: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[]{uri.getLastPathSegment()});
// fall through
case DISABLED_HOSTS:
trace("Delete on DISABLED_HOSTS: " + uri);
table = TABLE_DISABLED_HOSTS;
break;
default:
throw new UnsupportedOperationException("Unknown delete URI " + uri);
}
debug("Deleting " + table + " for URI: " + uri);
return db.delete(table, selection, selectionArgs);
}
@Override
@SuppressWarnings("fallthrough")
protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Calling update in transaction on URI: " + uri);
final int match = URI_MATCHER.match(uri);
final SQLiteDatabase db = getWritableDatabase(uri);
final String table;
beginWrite(db);
switch (match) {
case LOGINS_ID:
trace("Update on LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[]{Long.toString(ContentUris.parseId(uri))});
case LOGINS:
trace("Update on LOGINS: " + uri);
table = TABLE_LOGINS;
// Encrypt sensitive data.
encryptContentValueFields(values);
break;
default:
throw new UnsupportedOperationException("Unknown update URI " + uri);
}
trace("Updating " + table + " on URI: " + uri);
return db.update(table, values, selection, selectionArgs);
}
@Override
@SuppressWarnings("fallthrough")
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
trace("Calling query on URI: " + uri);
final SQLiteDatabase db = getReadableDatabase(uri);
final int match = URI_MATCHER.match(uri);
final String groupBy = null;
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
final String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
switch (match) {
case LOGINS_ID:
trace("Query is on LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case LOGINS:
trace("Query is on LOGINS: " + uri);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_LOGINS_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
qb.setProjectionMap(LOGIN_PROJECTION_MAP);
qb.setTables(TABLE_LOGINS);
break;
case DELETED_LOGINS_ID:
trace("Query is on DELETED_LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DELETED_LOGINS, DeletedLogins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case DELETED_LOGINS:
trace("Query is on DELETED_LOGINS: " + uri);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_DELETED_LOGINS_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
qb.setProjectionMap(DELETED_LOGIN_PROJECTION_MAP);
qb.setTables(TABLE_DELETED_LOGINS);
break;
case DISABLED_HOSTS_HOSTNAME:
trace("Query is on DISABLED_HOSTS_HOSTNAME: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { uri.getLastPathSegment() });
// fall through
case DISABLED_HOSTS:
trace("Query is on DISABLED_HOSTS: " + uri);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_DISABLED_HOSTS_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
qb.setProjectionMap(DISABLED_HOSTS_PROJECTION_MAP);
qb.setTables(TABLE_DISABLED_HOSTS);
break;
default:
throw new UnsupportedOperationException("Unknown query URI " + uri);
}
trace("Running built query.");
Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
// If decryptManyCursorRows does not return the original cursor, it closes it, so there's
// no need to close here.
cursor = decryptManyCursorRows(cursor);
cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.LOGINS_AUTHORITY_URI);
return cursor;
}
@Override
public String getType(@NonNull Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case LOGINS:
return Logins.CONTENT_TYPE;
case LOGINS_ID:
return Logins.CONTENT_ITEM_TYPE;
case DELETED_LOGINS:
return DeletedLogins.CONTENT_TYPE;
case DELETED_LOGINS_ID:
return DeletedLogins.CONTENT_ITEM_TYPE;
case DISABLED_HOSTS:
return LoginsDisabledHosts.CONTENT_TYPE;
case DISABLED_HOSTS_HOSTNAME:
return LoginsDisabledHosts.CONTENT_ITEM_TYPE;
default:
throw new UnsupportedOperationException("Unknown type " + uri);
}
}
/**
* Caller is responsible for invoking this method inside a transaction.
*/
private String getLoginGUIDByID(final String selection, final String[] selectionArgs, final SQLiteDatabase db) {
final Cursor cursor = db.query(Logins.TABLE_LOGINS, new String[]{Logins.GUID}, selection, selectionArgs, null, null, DEFAULT_LOGINS_SORT_ORDER);
try {
if (!cursor.moveToFirst()) {
return null;
}
return cursor.getString(cursor.getColumnIndexOrThrow(Logins.GUID));
} finally {
cursor.close();
}
}
/**
* Caller is responsible for invoking this method inside a transaction.
*/
private boolean storeDeletedLoginForGUIDInTransaction(final String guid, final SQLiteDatabase db) {
if (guid == null) {
return false;
}
final ContentValues values = new ContentValues();
values.put(DeletedLogins.GUID, guid);
values.put(DeletedLogins.TIME_DELETED, System.currentTimeMillis());
return db.insert(TABLE_DELETED_LOGINS, DeletedLogins.GUID, values) > 0;
}
/**
* Caller is responsible for invoking this method inside a transaction.
*/
private void removeDeletedLoginsByGUIDInTransaction(ContentValues values, SQLiteDatabase db) {
if (values.containsKey(Logins.GUID)) {
final String guid = values.getAsString(Logins.GUID);
if (guid == null) {
db.delete(TABLE_DELETED_LOGINS, WHERE_GUID_IS_NULL, null);
} else {
String[] args = new String[]{guid};
db.delete(TABLE_DELETED_LOGINS, WHERE_GUID_IS_VALUE, args);
}
}
}
private void setupDefaultValues(ContentValues values, Uri uri) throws IllegalArgumentException {
final int match = URI_MATCHER.match(uri);
final long now = System.currentTimeMillis();
switch (match) {
case DELETED_LOGINS:
values.put(DeletedLogins.TIME_DELETED, now);
// deleted-logins must contain a guid
if (!values.containsKey(DeletedLogins.GUID)) {
throw new IllegalArgumentException("Must provide GUID for deleted-login");
}
break;
case LOGINS:
values.put(Logins.TIME_CREATED, now);
// Generate GUID for new login. Don't override specified GUIDs.
if (!values.containsKey(Logins.GUID)) {
final String guid = Utils.generateGuid();
values.put(Logins.GUID, guid);
}
// The database happily accepts strings for long values; this just lets us re-use
// the existing helper method.
String nowString = Long.toString(now);
DBUtils.replaceKey(values, null, Logins.HTTP_REALM, null);
DBUtils.replaceKey(values, null, Logins.FORM_SUBMIT_URL, null);
DBUtils.replaceKey(values, null, Logins.ENC_TYPE, "0");
DBUtils.replaceKey(values, null, Logins.TIME_LAST_USED, nowString);
DBUtils.replaceKey(values, null, Logins.TIME_PASSWORD_CHANGED, nowString);
DBUtils.replaceKey(values, null, Logins.TIMES_USED, "0");
break;
case DISABLED_HOSTS:
if (!values.containsKey(LoginsDisabledHosts.HOSTNAME)) {
throw new IllegalArgumentException("Must provide hostname for disabled-host");
}
break;
default:
throw new UnsupportedOperationException("Unknown URI in setupDefaultValues " + uri);
}
}
private void encryptContentValueFields(final ContentValues values) {
if (values.containsKey(Logins.ENCRYPTED_PASSWORD)) {
final String res = encrypt(values.getAsString(Logins.ENCRYPTED_PASSWORD));
values.put(Logins.ENCRYPTED_PASSWORD, res);
}
if (values.containsKey(Logins.ENCRYPTED_USERNAME)) {
final String res = encrypt(values.getAsString(Logins.ENCRYPTED_USERNAME));
values.put(Logins.ENCRYPTED_USERNAME, res);
}
}
/**
* Replace each password and username encrypted ciphertext with its equivalent decrypted
* plaintext in the given cursor.
* <p/>
* The encryption algorithm used to protect logins is unspecified; and further, a consumer of
* consumers should never have access to encrypted ciphertext.
*
* @param cursor containing at least one of password and username encrypted ciphertexts.
* @return a new {@link Cursor} with password and username decrypted plaintexts.
*/
private Cursor decryptManyCursorRows(final Cursor cursor) {
final int passwordIndex = cursor.getColumnIndex(Logins.ENCRYPTED_PASSWORD);
final int usernameIndex = cursor.getColumnIndex(Logins.ENCRYPTED_USERNAME);
if (passwordIndex == -1 && usernameIndex == -1) {
return cursor;
}
// Special case, decrypt the encrypted username or password before returning the cursor.
final MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames(), cursor.getColumnCount());
try {
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
final ContentValues values = new ContentValues();
DatabaseUtils.cursorRowToContentValues(cursor, values);
if (passwordIndex > -1) {
String decrypted = decrypt(values.getAsString(Logins.ENCRYPTED_PASSWORD));
values.put(Logins.ENCRYPTED_PASSWORD, decrypted);
}
if (usernameIndex > -1) {
String decrypted = decrypt(values.getAsString(Logins.ENCRYPTED_USERNAME));
values.put(Logins.ENCRYPTED_USERNAME, decrypted);
}
final MatrixCursor.RowBuilder rowBuilder = newCursor.newRow();
for (String key : cursor.getColumnNames()) {
rowBuilder.add(values.get(key));
}
}
} finally {
// Close the old cursor before returning the new one.
cursor.close();
}
return newCursor;
}
private String encrypt(@NonNull String initialValue) {
try {
final Cipher cipher = getCipher(Cipher.ENCRYPT_MODE);
return Base64.encodeToString(cipher.doFinal(initialValue.getBytes("UTF-8")), Base64.URL_SAFE);
} catch (Exception e) {
debug("encryption failed : " + e);
throw new IllegalStateException("Logins encryption failed", e);
}
}
private String decrypt(@NonNull String initialValue) {
try {
final Cipher cipher = getCipher(Cipher.DECRYPT_MODE);
return new String(cipher.doFinal(Base64.decode(initialValue.getBytes("UTF-8"), Base64.URL_SAFE)));
} catch (Exception e) {
debug("Decryption failed : " + e);
throw new IllegalStateException("Logins decryption failed", e);
}
}
private Cipher getCipher(int mode) throws UnsupportedEncodingException, GeneralSecurityException {
return new NullCipher();
}
}

View file

@ -0,0 +1,348 @@
/* 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.db;
import java.util.HashMap;
import org.mozilla.gecko.CrashHandler;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoMessageReceiver;
import org.mozilla.gecko.NSSBridge;
import org.mozilla.gecko.db.BrowserContract.DeletedPasswords;
import org.mozilla.gecko.db.BrowserContract.GeckoDisabledHosts;
import org.mozilla.gecko.db.BrowserContract.Passwords;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.sqlite.MatrixBlobCursor;
import org.mozilla.gecko.sqlite.SQLiteBridge;
import org.mozilla.gecko.sync.Utils;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class PasswordsProvider extends SQLiteBridgeContentProvider {
static final String TABLE_PASSWORDS = "moz_logins";
static final String TABLE_DELETED_PASSWORDS = "moz_deleted_logins";
static final String TABLE_DISABLED_HOSTS = "moz_disabledHosts";
private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_PASSWORDS";
private static final int PASSWORDS = 100;
private static final int DELETED_PASSWORDS = 101;
private static final int DISABLED_HOSTS = 102;
static final String DEFAULT_PASSWORDS_SORT_ORDER = Passwords.HOSTNAME + " ASC";
static final String DEFAULT_DELETED_PASSWORDS_SORT_ORDER = DeletedPasswords.TIME_DELETED + " ASC";
private static final UriMatcher URI_MATCHER;
private static final HashMap<String, String> PASSWORDS_PROJECTION_MAP;
private static final HashMap<String, String> DELETED_PASSWORDS_PROJECTION_MAP;
private static final HashMap<String, String> DISABLED_HOSTS_PROJECTION_MAP;
// this should be kept in sync with the version in toolkit/components/passwordmgr/storage-mozStorage.js
private static final int DB_VERSION = 6;
private static final String DB_FILENAME = "signons.sqlite";
private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedPasswords.GUID + " IS NULL";
private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedPasswords.GUID + " = ?";
private static final String LOG_TAG = "GeckoPasswordsProvider";
private CrashHandler mCrashHandler;
static {
URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
// content://org.mozilla.gecko.providers.browser/passwords/#
URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "passwords", PASSWORDS);
PASSWORDS_PROJECTION_MAP = new HashMap<String, String>();
PASSWORDS_PROJECTION_MAP.put(Passwords.ID, Passwords.ID);
PASSWORDS_PROJECTION_MAP.put(Passwords.HOSTNAME, Passwords.HOSTNAME);
PASSWORDS_PROJECTION_MAP.put(Passwords.HTTP_REALM, Passwords.HTTP_REALM);
PASSWORDS_PROJECTION_MAP.put(Passwords.FORM_SUBMIT_URL, Passwords.FORM_SUBMIT_URL);
PASSWORDS_PROJECTION_MAP.put(Passwords.USERNAME_FIELD, Passwords.USERNAME_FIELD);
PASSWORDS_PROJECTION_MAP.put(Passwords.PASSWORD_FIELD, Passwords.PASSWORD_FIELD);
PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_USERNAME, Passwords.ENCRYPTED_USERNAME);
PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_PASSWORD, Passwords.ENCRYPTED_PASSWORD);
PASSWORDS_PROJECTION_MAP.put(Passwords.GUID, Passwords.GUID);
PASSWORDS_PROJECTION_MAP.put(Passwords.ENC_TYPE, Passwords.ENC_TYPE);
PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_CREATED, Passwords.TIME_CREATED);
PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_LAST_USED, Passwords.TIME_LAST_USED);
PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_PASSWORD_CHANGED, Passwords.TIME_PASSWORD_CHANGED);
PASSWORDS_PROJECTION_MAP.put(Passwords.TIMES_USED, Passwords.TIMES_USED);
URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "deleted-passwords", DELETED_PASSWORDS);
DELETED_PASSWORDS_PROJECTION_MAP = new HashMap<String, String>();
DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.ID, DeletedPasswords.ID);
DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.GUID, DeletedPasswords.GUID);
DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.TIME_DELETED, DeletedPasswords.TIME_DELETED);
URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "disabled-hosts", DISABLED_HOSTS);
DISABLED_HOSTS_PROJECTION_MAP = new HashMap<String, String>();
DISABLED_HOSTS_PROJECTION_MAP.put(GeckoDisabledHosts.HOSTNAME, GeckoDisabledHosts.HOSTNAME);
}
public PasswordsProvider() {
super(LOG_TAG);
}
@Override
public boolean onCreate() {
mCrashHandler = CrashHandler.createDefaultCrashHandler(getContext());
// We don't use .loadMozGlue because we're in a different process,
// and we just want to reuse code rather than use the loader lock etc.
GeckoLoader.doLoadLibrary(getContext(), "mozglue");
return super.onCreate();
}
@Override
public void shutdown() {
super.shutdown();
if (mCrashHandler != null) {
mCrashHandler.unregister();
mCrashHandler = null;
}
}
@Override
protected String getDBName() {
return DB_FILENAME;
}
@Override
protected String getTelemetryPrefix() {
return TELEMETRY_TAG;
}
@Override
protected int getDBVersion() {
return DB_VERSION;
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case PASSWORDS:
return Passwords.CONTENT_TYPE;
case DELETED_PASSWORDS:
return DeletedPasswords.CONTENT_TYPE;
case DISABLED_HOSTS:
return GeckoDisabledHosts.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown type " + uri);
}
}
@Override
public String getTable(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case DELETED_PASSWORDS:
return TABLE_DELETED_PASSWORDS;
case PASSWORDS:
return TABLE_PASSWORDS;
case DISABLED_HOSTS:
return TABLE_DISABLED_HOSTS;
default:
throw new UnsupportedOperationException("Unknown table " + uri);
}
}
@Override
public String getSortOrder(Uri uri, String aRequested) {
if (!TextUtils.isEmpty(aRequested)) {
return aRequested;
}
final int match = URI_MATCHER.match(uri);
switch (match) {
case DELETED_PASSWORDS:
return DEFAULT_DELETED_PASSWORDS_SORT_ORDER;
case PASSWORDS:
return DEFAULT_PASSWORDS_SORT_ORDER;
case DISABLED_HOSTS:
return null;
default:
throw new UnsupportedOperationException("Unknown URI " + uri);
}
}
@Override
public void setupDefaults(Uri uri, ContentValues values)
throws IllegalArgumentException {
int match = URI_MATCHER.match(uri);
long now = System.currentTimeMillis();
switch (match) {
case DELETED_PASSWORDS:
values.put(DeletedPasswords.TIME_DELETED, now);
// Deleted passwords must contain a guid
if (!values.containsKey(Passwords.GUID)) {
throw new IllegalArgumentException("Must provide a GUID for a deleted password");
}
break;
case PASSWORDS:
values.put(Passwords.TIME_CREATED, now);
// Generate GUID for new password. Don't override specified GUIDs.
if (!values.containsKey(Passwords.GUID)) {
String guid = Utils.generateGuid();
values.put(Passwords.GUID, guid);
}
String nowString = Long.toString(now);
DBUtils.replaceKey(values, null, Passwords.HOSTNAME, "");
DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, "");
DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, "");
DBUtils.replaceKey(values, null, Passwords.USERNAME_FIELD, "");
DBUtils.replaceKey(values, null, Passwords.PASSWORD_FIELD, "");
DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_USERNAME, "");
DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_PASSWORD, "");
DBUtils.replaceKey(values, null, Passwords.ENC_TYPE, "0");
DBUtils.replaceKey(values, null, Passwords.TIME_LAST_USED, nowString);
DBUtils.replaceKey(values, null, Passwords.TIME_PASSWORD_CHANGED, nowString);
DBUtils.replaceKey(values, null, Passwords.TIMES_USED, "0");
break;
case DISABLED_HOSTS:
if (!values.containsKey(GeckoDisabledHosts.HOSTNAME)) {
throw new IllegalArgumentException("Must provide a hostname for a disabled host");
}
break;
default:
throw new UnsupportedOperationException("Unknown URI " + uri);
}
}
@Override
public void initGecko() {
// We're not in the main process. The receiver of this Intent can
// communicate with Gecko in the main process.
Intent initIntent = new Intent(getContext(), GeckoMessageReceiver.class);
initIntent.setAction(GeckoApp.ACTION_INIT_PW);
mContext.sendBroadcast(initIntent);
}
private String doCrypto(String initialValue, Uri uri, Boolean encrypt) {
String profilePath = null;
if (uri != null) {
profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH);
}
String result = "";
try {
if (encrypt) {
if (profilePath != null) {
result = NSSBridge.encrypt(mContext, profilePath, initialValue);
} else {
result = NSSBridge.encrypt(mContext, initialValue);
}
} else {
if (profilePath != null) {
result = NSSBridge.decrypt(mContext, profilePath, initialValue);
} else {
result = NSSBridge.decrypt(mContext, initialValue);
}
}
} catch (Exception ex) {
Log.e(LOG_TAG, "Error in NSSBridge");
throw new RuntimeException(ex);
}
return result;
}
@Override
public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
if (values.containsKey(Passwords.GUID)) {
String guid = values.getAsString(Passwords.GUID);
if (guid == null) {
db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_NULL, null);
return;
}
String[] args = new String[] { guid };
db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_VALUE, args);
}
if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) {
String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true);
values.put(Passwords.ENCRYPTED_PASSWORD, res);
values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
}
if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) {
String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true);
values.put(Passwords.ENCRYPTED_USERNAME, res);
values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
}
}
@Override
public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) {
if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) {
String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true);
values.put(Passwords.ENCRYPTED_PASSWORD, res);
values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
}
if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) {
String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true);
values.put(Passwords.ENCRYPTED_USERNAME, res);
values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
}
}
@Override
public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) {
int passwordIndex = -1;
int usernameIndex = -1;
String profilePath = null;
try {
passwordIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_PASSWORD);
} catch (Exception ex) { }
try {
usernameIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_USERNAME);
} catch (Exception ex) { }
if (passwordIndex > -1 || usernameIndex > -1) {
MatrixBlobCursor m = (MatrixBlobCursor)cursor;
if (cursor.moveToFirst()) {
do {
if (passwordIndex > -1) {
String decrypted = doCrypto(cursor.getString(passwordIndex), uri, false);;
m.set(passwordIndex, decrypted);
}
if (usernameIndex > -1) {
String decrypted = doCrypto(cursor.getString(usernameIndex), uri, false);
m.set(usernameIndex, decrypted);
}
} while (cursor.moveToNext());
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more