--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -66,16 +66,17 @@ android {
}
}
sourceSets {
main {
manifest.srcFile "${project.buildDir}/generated/source/preprocessed_manifest/AndroidManifest.xml"
java {
+ srcDir "${topsrcdir}/mobile/android/geckoview/src/main/java"
srcDir "${topsrcdir}/mobile/android/base/java"
srcDir "${topsrcdir}/mobile/android/search/java"
srcDir "${topsrcdir}/mobile/android/javaaddons/java"
srcDir "${topsrcdir}/mobile/android/services/src/main/java"
if (mozconfig.substs.MOZ_ANDROID_MLS_STUMBLER) {
srcDir "${topsrcdir}/mobile/android/stumbler/java"
}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
+++ /dev/null
@@ -1,596 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-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);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/AlarmReceiver.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/* -*- 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 android.app.IntentService;
-import android.content.Context;
-import android.content.Intent;
-import android.content.BroadcastReceiver;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.util.Log;
-
-import java.util.Timer;
-import java.util.TimerTask;
-
-public class AlarmReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
- final WakeLock wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "GeckoAlarm");
- wakeLock.acquire();
-
- AlarmReceiver.notifyAlarmFired();
- TimerTask releaseLockTask = new TimerTask() {
- @Override
- public void run() {
- wakeLock.release();
- }
- };
- Timer timer = new Timer();
- // 5 seconds ought to be enough for anybody
- timer.schedule(releaseLockTask, 5 * 1000);
- }
-
- @WrapForJNI
- private static native void notifyAlarmFired();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/AndroidGamepadManager.java
+++ /dev/null
@@ -1,392 +0,0 @@
-/* -*- 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.util.ArrayList;
-import java.util.List;
-import java.util.Timer;
-import java.util.TimerTask;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.util.GamepadUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.hardware.input.InputManager;
-import android.util.SparseArray;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
-
-public class AndroidGamepadManager {
- // This is completely arbitrary.
- private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
- private static final long POLL_TIMER_PERIOD = 1000; // milliseconds
-
- private static enum Axis {
- X(MotionEvent.AXIS_X),
- Y(MotionEvent.AXIS_Y),
- Z(MotionEvent.AXIS_Z),
- RZ(MotionEvent.AXIS_RZ);
-
- public final int axis;
-
- private Axis(int axis) {
- this.axis = axis;
- }
- }
-
- // A list of gamepad button mappings. Axes are determined at
- // runtime, as they vary by Android version.
- private static enum Trigger {
- Left(6),
- Right(7);
-
- public final int button;
-
- private Trigger(int button) {
- this.button = button;
- }
- }
-
- private static final int FIRST_DPAD_BUTTON = 12;
- // A list of axis number, gamepad button mappings for negative, positive.
- // Button mappings are added to FIRST_DPAD_BUTTON.
- private static enum DpadAxis {
- UpDown(MotionEvent.AXIS_HAT_Y, 0, 1),
- LeftRight(MotionEvent.AXIS_HAT_X, 2, 3);
-
- public final int axis;
- public final int negativeButton;
- public final int positiveButton;
-
- private DpadAxis(int axis, int negativeButton, int positiveButton) {
- this.axis = axis;
- this.negativeButton = negativeButton;
- this.positiveButton = positiveButton;
- }
- }
-
- private static enum Button {
- A(KeyEvent.KEYCODE_BUTTON_A),
- B(KeyEvent.KEYCODE_BUTTON_B),
- X(KeyEvent.KEYCODE_BUTTON_X),
- Y(KeyEvent.KEYCODE_BUTTON_Y),
- L1(KeyEvent.KEYCODE_BUTTON_L1),
- R1(KeyEvent.KEYCODE_BUTTON_R1),
- L2(KeyEvent.KEYCODE_BUTTON_L2),
- R2(KeyEvent.KEYCODE_BUTTON_R2),
- SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
- START(KeyEvent.KEYCODE_BUTTON_START),
- THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
- THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
- DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
- DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
- DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
- DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT);
-
- public final int button;
-
- private Button(int button) {
- this.button = button;
- }
- }
-
- private static class Gamepad {
- // ID from GamepadService
- public int id;
- // Retain axis state so we can determine changes.
- public float axes[];
- public boolean dpad[];
- public int triggerAxes[];
- public float triggers[];
-
- public Gamepad(int serviceId, int deviceId) {
- id = serviceId;
- axes = new float[Axis.values().length];
- dpad = new boolean[4];
- triggers = new float[2];
-
- InputDevice device = InputDevice.getDevice(deviceId);
- if (device != null) {
- // LTRIGGER/RTRIGGER don't seem to be exposed on older
- // versions of Android.
- if (device.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null && device.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null) {
- triggerAxes = new int[]{MotionEvent.AXIS_LTRIGGER,
- MotionEvent.AXIS_RTRIGGER};
- } else if (device.getMotionRange(MotionEvent.AXIS_BRAKE) != null && device.getMotionRange(MotionEvent.AXIS_GAS) != null) {
- triggerAxes = new int[]{MotionEvent.AXIS_BRAKE,
- MotionEvent.AXIS_GAS};
- } else {
- triggerAxes = null;
- }
- }
- }
- }
-
- private static boolean sStarted;
- private static final SparseArray<Gamepad> sGamepads = new SparseArray<>();
- private static final SparseArray<List<KeyEvent>> sPendingGamepads = new SparseArray<>();
- private static InputManager.InputDeviceListener sListener;
- private static Timer sPollTimer;
-
- private AndroidGamepadManager() {
- }
-
- public static void startup() {
- ThreadUtils.assertOnUiThread();
- if (!sStarted) {
- scanForGamepads();
- addDeviceListener();
- sStarted = true;
- }
- }
-
- public static void shutdown() {
- ThreadUtils.assertOnUiThread();
- if (sStarted) {
- removeDeviceListener();
- sPendingGamepads.clear();
- sGamepads.clear();
- sStarted = false;
- }
- }
-
- public static void gamepadAdded(int deviceId, int serviceId) {
- ThreadUtils.assertOnUiThread();
- if (!sStarted) {
- return;
- }
-
- final List<KeyEvent> pending = sPendingGamepads.get(deviceId);
- if (pending == null) {
- removeGamepad(deviceId);
- return;
- }
-
- sPendingGamepads.remove(deviceId);
- sGamepads.put(deviceId, new Gamepad(serviceId, deviceId));
- // Handle queued KeyEvents
- for (KeyEvent ev : pending) {
- handleKeyEvent(ev);
- }
- }
-
- private static float deadZone(MotionEvent ev, int axis) {
- if (GamepadUtils.isValueInDeadZone(ev, axis)) {
- return 0.0f;
- }
- return ev.getAxisValue(axis);
- }
-
- private static void mapDpadAxis(Gamepad gamepad,
- boolean pressed,
- float value,
- int which) {
- if (pressed != gamepad.dpad[which]) {
- gamepad.dpad[which] = pressed;
- GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value)));
- }
- }
-
- public static boolean handleMotionEvent(MotionEvent ev) {
- ThreadUtils.assertOnUiThread();
- if (!sStarted) {
- return false;
- }
-
- final Gamepad gamepad = sGamepads.get(ev.getDeviceId());
- if (gamepad == null) {
- // Not a device we care about.
- return false;
- }
-
- // First check the analog stick axes
- boolean[] valid = new boolean[Axis.values().length];
- float[] axes = new float[Axis.values().length];
- boolean anyValidAxes = false;
- for (Axis axis : Axis.values()) {
- float value = deadZone(ev, axis.axis);
- int i = axis.ordinal();
- if (value != gamepad.axes[i]) {
- axes[i] = value;
- gamepad.axes[i] = value;
- valid[i] = true;
- anyValidAxes = true;
- }
- }
- if (anyValidAxes) {
- // Send an axismove event.
- GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAxisEvent(gamepad.id, valid, axes));
- }
-
- // Map triggers to buttons.
- if (gamepad.triggerAxes != null) {
- for (Trigger trigger : Trigger.values()) {
- int i = trigger.ordinal();
- int axis = gamepad.triggerAxes[i];
- float value = deadZone(ev, axis);
- if (value != gamepad.triggers[i]) {
- gamepad.triggers[i] = value;
- boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
- GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, trigger.button, pressed, value));
- }
- }
- }
- // Map d-pad to buttons.
- for (DpadAxis dpadaxis : DpadAxis.values()) {
- float value = deadZone(ev, dpadaxis.axis);
- mapDpadAxis(gamepad, value < 0.0f, value, dpadaxis.negativeButton);
- mapDpadAxis(gamepad, value > 0.0f, value, dpadaxis.positiveButton);
- }
- return true;
- }
-
- public static boolean handleKeyEvent(KeyEvent ev) {
- ThreadUtils.assertOnUiThread();
- if (!sStarted) {
- return false;
- }
-
- int deviceId = ev.getDeviceId();
- final List<KeyEvent> pendingGamepad = sPendingGamepads.get(deviceId);
- if (pendingGamepad != null) {
- // Queue up key events for pending devices.
- pendingGamepad.add(ev);
- return true;
- }
-
- if (sGamepads.get(deviceId) == null) {
- InputDevice device = ev.getDevice();
- if (device != null &&
- (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
- // This is a gamepad we haven't seen yet.
- addGamepad(device);
- sPendingGamepads.get(deviceId).add(ev);
- return true;
- }
- // Not a device we care about.
- return false;
- }
-
- int key = -1;
- for (Button button : Button.values()) {
- if (button.button == ev.getKeyCode()) {
- key = button.ordinal();
- break;
- }
- }
- if (key == -1) {
- // Not a key we know how to handle.
- return false;
- }
- if (ev.getRepeatCount() > 0) {
- // We would handle this key, but we're not interested in
- // repeats. Eat it.
- return true;
- }
-
- Gamepad gamepad = sGamepads.get(deviceId);
- boolean pressed = ev.getAction() == KeyEvent.ACTION_DOWN;
- GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, key, pressed, pressed ? 1.0f : 0.0f));
- return true;
- }
-
- private static void scanForGamepads() {
- int[] deviceIds = InputDevice.getDeviceIds();
- if (deviceIds == null) {
- return;
- }
- for (int i = 0; i < deviceIds.length; i++) {
- InputDevice device = InputDevice.getDevice(deviceIds[i]);
- if (device == null) {
- continue;
- }
- if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
- continue;
- }
- addGamepad(device);
- }
- }
-
- private static void addGamepad(InputDevice device) {
- //TODO: when we're using a newer SDK version, use these.
- //if (Build.VERSION.SDK_INT >= 12) {
- //int vid = device.getVendorId();
- //int pid = device.getProductId();
- //}
- sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
- GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(device.getId(), true));
- }
-
- private static void removeGamepad(int deviceId) {
- Gamepad gamepad = sGamepads.get(deviceId);
- GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(gamepad.id, false));
- sGamepads.remove(deviceId);
- }
-
- private static void addDeviceListener() {
- if (Versions.preJB) {
- // Poll known gamepads to see if they've disappeared.
- sPollTimer = new Timer();
- sPollTimer.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- for (int i = 0; i < sGamepads.size(); ++i) {
- final int deviceId = sGamepads.keyAt(i);
- if (InputDevice.getDevice(deviceId) == null) {
- removeGamepad(deviceId);
- }
- }
- }
- }, POLL_TIMER_PERIOD, POLL_TIMER_PERIOD);
- return;
- }
- sListener = new InputManager.InputDeviceListener() {
- @Override
- public void onInputDeviceAdded(int deviceId) {
- InputDevice device = InputDevice.getDevice(deviceId);
- if (device == null) {
- return;
- }
- if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
- addGamepad(device);
- }
- }
-
- @Override
- public void onInputDeviceRemoved(int deviceId) {
- if (sPendingGamepads.get(deviceId) != null) {
- // Got removed before Gecko's ack reached us.
- // gamepadAdded will deal with it.
- sPendingGamepads.remove(deviceId);
- return;
- }
- if (sGamepads.get(deviceId) != null) {
- removeGamepad(deviceId);
- }
- }
-
- @Override
- public void onInputDeviceChanged(int deviceId) {
- }
- };
- ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).registerInputDeviceListener(sListener, ThreadUtils.getUiHandler());
- }
-
- private static void removeDeviceListener() {
- if (Versions.preJB) {
- if (sPollTimer != null) {
- sPollTimer.cancel();
- sPollTimer = null;
- }
- return;
- }
- ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).unregisterInputDeviceListener(sListener);
- sListener = null;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/BaseGeckoInterface.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.util.ActivityUtils;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.RectF;
-import android.hardware.SensorEventListener;
-import android.location.LocationListener;
-import android.view.View;
-import android.widget.AbsoluteLayout;
-
-public class BaseGeckoInterface implements GeckoAppShell.GeckoInterface {
- // Bug 908744: Implement GeckoEventListener
- // Bug 908752: Implement SensorEventListener
- // Bug 908755: Implement LocationListener
- // Bug 908756: Implement Tabs.OnTabsChangedListener
- // Bug 908760: Implement GeckoEventResponder
-
- private final Context mContext;
- private GeckoProfile mProfile;
-
- public BaseGeckoInterface(Context context) {
- mContext = context;
- }
-
- @Override
- public GeckoProfile getProfile() {
- // Fall back to default profile if we didn't load a specific one
- if (mProfile == null) {
- mProfile = GeckoProfile.get(mContext);
- }
- return mProfile;
- }
-
- @Override
- public Activity getActivity() {
- return (Activity)mContext;
- }
-
- @Override
- public String getDefaultUAString() {
- return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
- AppConstants.USER_AGENT_FENNEC_MOBILE;
- }
-
- // Bug 908772: Implement this
- @Override
- public LocationListener getLocationListener() {
- return null;
- }
-
- // Bug 908773: Implement this
- @Override
- public SensorEventListener getSensorEventListener() {
- return null;
- }
-
- // Bug 908775: Implement this
- @Override
- public void doRestart() {}
-
- @Override
- public void setFullScreen(final boolean fullscreen) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- ActivityUtils.setFullScreen(getActivity(), fullscreen);
- }
- });
- }
-
- // Bug 908779: Implement this
- @Override
- public void addPluginView(final View view, final RectF rect, final boolean isFullScreen) {}
-
- // Bug 908781: Implement this
- @Override
- public void removePluginView(final View view, final boolean isFullScreen) {}
-
- // Bug 908783: Implement this
- @Override
- public void enableCameraView() {}
-
- // Bug 908785: Implement this
- @Override
- public void disableCameraView() {}
-
- // Bug 908786: Implement this
- @Override
- public void addAppStateListener(GeckoAppShell.AppStateListener listener) {}
-
- // Bug 908787: Implement this
- @Override
- public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {}
-
- // Bug 908788: Implement this
- @Override
- public View getCameraView() {
- return null;
- }
-
- // Bug 908789: Implement this
- @Override
- public void notifyWakeLockChanged(String topic, String state) {}
-
- // Bug 908790: Implement this
- @Override
- public void onInputMethodChanged(String inputMethod) {}
-
- @Override
- public boolean areTabsShown() {
- return false;
- }
-
- // Bug 908791: Implement this
- @Override
- public AbsoluteLayout getPluginContainer() {
- return null;
- }
-
- @Override
- public void notifyCheckUpdateResult(String result) {
- GeckoAppShell.notifyObservers("Update:CheckResult", result);
- }
-
- // Bug 908792: Implement this
- @Override
- public void invalidateOptionsMenu() {}
-
- @Override
- public void createShortcut(String title, String URI) {
- // By default, do nothing.
- }
-
- @Override
- public void checkUriVisited(String uri) {
- // By default, no URIs are considered visited.
- }
-
- @Override
- public void markUriVisited(final String uri) {
- // By default, no URIs are marked as visited.
- }
-
- @Override
- public void setUriTitle(final String uri, final String title) {
- // By default, no titles are associated with URIs.
- }
-
- @Override
- public void setAccessibilityEnabled(boolean enabled) {
- // By default, take no action when accessibility is toggled on or off.
- }
-
- @Override
- public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title) {
- // By default, never open external URIs.
- return false;
- }
-
- @Override
- public String[] getHandlersForMimeType(String mimeType, String action) {
- // By default, offer no handlers for any MIME type.
- return new String[] {};
- }
-
- @Override
- public String[] getHandlersForURL(String url, String action) {
- // By default, offer no handlers for any URL.
- return new String[] {};
- }
-
- @Override
- public String getDefaultChromeURI() {
- // By default, use the GeckoView-specific chrome URI.
- return "chrome://browser/content/geckoview.xul";
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/ContextGetter.java
+++ /dev/null
@@ -1,15 +0,0 @@
-/* -*- 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.content.Context;
-import android.content.SharedPreferences;
-
-public interface ContextGetter {
- Context getContext();
- SharedPreferences getSharedPreferences();
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/CrashHandler.java
+++ /dev/null
@@ -1,469 +0,0 @@
-/* -*- 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.BufferedWriter;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.Arrays;
-import java.util.UUID;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Process;
-import android.util.Log;
-
-public class CrashHandler implements Thread.UncaughtExceptionHandler {
-
- private static final String LOGTAG = "GeckoCrashHandler";
- private static final Thread MAIN_THREAD = Thread.currentThread();
- private static final String DEFAULT_SERVER_URL =
- "https://crash-reports.mozilla.com/submit?id=%1$s&version=%2$s&buildid=%3$s";
-
- // Context for getting device information
- protected final Context appContext;
- // Thread that this handler applies to, or null for a global handler
- protected final Thread handlerThread;
- protected final Thread.UncaughtExceptionHandler systemUncaughtHandler;
-
- protected boolean crashing;
- protected boolean unregistered;
-
- /**
- * Get the root exception from the 'cause' chain of an exception.
- *
- * @param exc An exception
- * @return The root exception
- */
- public static Throwable getRootException(Throwable exc) {
- for (Throwable cause = exc; cause != null; cause = cause.getCause()) {
- exc = cause;
- }
- return exc;
- }
-
- /**
- * Get the standard stack trace string of an exception.
- *
- * @param exc An exception
- * @return The exception stack trace.
- */
- public static String getExceptionStackTrace(final Throwable exc) {
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- exc.printStackTrace(pw);
- pw.flush();
- return sw.toString();
- }
-
- /**
- * Terminate the current process.
- */
- public static void terminateProcess() {
- Process.killProcess(Process.myPid());
- }
-
- /**
- * Create and register a CrashHandler for all threads and thread groups.
- */
- public CrashHandler() {
- this((Context) null);
- }
-
- /**
- * Create and register a CrashHandler for all threads and thread groups.
- *
- * @param appContext A Context for retrieving application information.
- */
- public CrashHandler(final Context appContext) {
- this.appContext = appContext;
- this.handlerThread = null;
- this.systemUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();
- Thread.setDefaultUncaughtExceptionHandler(this);
- }
-
- /**
- * Create and register a CrashHandler for a particular thread.
- *
- * @param thread A thread to register the CrashHandler
- */
- public CrashHandler(final Thread thread) {
- this(thread, null);
- }
-
- /**
- * Create and register a CrashHandler for a particular thread.
- *
- * @param thread A thread to register the CrashHandler
- * @param appContext A Context for retrieving application information.
- */
- public CrashHandler(final Thread thread, final Context appContext) {
- this.appContext = appContext;
- this.handlerThread = thread;
- this.systemUncaughtHandler = thread.getUncaughtExceptionHandler();
- thread.setUncaughtExceptionHandler(this);
- }
-
- /**
- * Unregister this CrashHandler for exception handling.
- */
- public void unregister() {
- unregistered = true;
-
- // Restore the previous handler if we are still the topmost handler.
- // If not, we are part of a chain of handlers, and we cannot just restore the previous
- // handler, because that would replace whatever handler that's above us in the chain.
-
- if (handlerThread != null) {
- if (handlerThread.getUncaughtExceptionHandler() == this) {
- handlerThread.setUncaughtExceptionHandler(systemUncaughtHandler);
- }
- } else {
- if (Thread.getDefaultUncaughtExceptionHandler() == this) {
- Thread.setDefaultUncaughtExceptionHandler(systemUncaughtHandler);
- }
- }
- }
-
- /**
- * Record an exception stack in logs.
- *
- * @param thread The exception thread
- * @param exc An exception
- */
- public static void logException(final Thread thread, final Throwable exc) {
- try {
- Log.e(LOGTAG, ">>> REPORTING UNCAUGHT EXCEPTION FROM THREAD "
- + thread.getId() + " (\"" + thread.getName() + "\")", exc);
-
- if (MAIN_THREAD != thread) {
- Log.e(LOGTAG, "Main thread (" + MAIN_THREAD.getId() + ") stack:");
- for (StackTraceElement ste : MAIN_THREAD.getStackTrace()) {
- Log.e(LOGTAG, " " + ste.toString());
- }
- }
- } catch (final Throwable e) {
- // If something throws here, we want to continue to report the exception,
- // so we catch all exceptions and ignore them.
- }
- }
-
- private static long getCrashTime() {
- return System.currentTimeMillis() / 1000;
- }
-
- private static long getStartupTime() {
- // Process start time is also the proc file modified time.
- final long uptimeMins = (new File("/proc/self/cmdline")).lastModified();
- if (uptimeMins == 0L) {
- return getCrashTime();
- }
- return uptimeMins / 1000;
- }
-
- private static String getJavaPackageName() {
- return CrashHandler.class.getPackage().getName();
- }
-
- protected String getAppPackageName() {
- final Context context = getAppContext();
-
- if (context != null) {
- return context.getPackageName();
- }
-
- try {
- // Package name is also the command line string in most cases.
- final FileReader reader = new FileReader("/proc/self/cmdline");
- final char[] buffer = new char[64];
- try {
- if (reader.read(buffer) > 0) {
- // cmdline is delimited by '\0', and we want the first token.
- final int nul = Arrays.asList(buffer).indexOf('\0');
- return (new String(buffer, 0, nul < 0 ? buffer.length : nul)).trim();
- }
- } finally {
- reader.close();
- }
-
- } catch (final IOException e) {
- Log.i(LOGTAG, "Error reading package name", e);
- }
-
- // Fallback to using CrashHandler's package name.
- return getJavaPackageName();
- }
-
- protected Context getAppContext() {
- return appContext;
- }
-
- /**
- * Get the crash "extras" to be reported.
- *
- * @param thread The exception thread
- * @param exc An exception
- * @return "Extras" in the from of a Bundle
- */
- protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
- final Context context = getAppContext();
- final Bundle extras = new Bundle();
- final String pkgName = getAppPackageName();
-
- extras.putString("ProductName", pkgName);
- extras.putLong("CrashTime", getCrashTime());
- extras.putLong("StartupTime", getStartupTime());
-
- if (context != null) {
- final PackageManager pkgMgr = context.getPackageManager();
- try {
- final PackageInfo pkgInfo = pkgMgr.getPackageInfo(pkgName, 0);
- extras.putString("Version", pkgInfo.versionName);
- extras.putInt("BuildID", pkgInfo.versionCode);
- extras.putLong("InstallTime", pkgInfo.lastUpdateTime / 1000);
- } catch (final PackageManager.NameNotFoundException e) {
- Log.i(LOGTAG, "Error getting package info", e);
- }
- }
-
- extras.putString("JavaStackTrace", getExceptionStackTrace(exc));
- return extras;
- }
-
- /**
- * Get the crash minidump content to be reported.
- *
- * @param thread The exception thread
- * @param exc An exception
- * @return Minidump content
- */
- protected byte[] getCrashDump(final Thread thread, final Throwable exc) {
- return new byte[0]; // No minidump.
- }
-
- protected static String normalizeUrlString(final String str) {
- if (str == null) {
- return "";
- }
- return Uri.encode(str);
- }
-
- /**
- * Get the server URL to send the crash report to.
- *
- * @param extras The crash extras Bundle
- */
- protected String getServerUrl(final Bundle extras) {
- return String.format(DEFAULT_SERVER_URL,
- normalizeUrlString(extras.getString("ProductID")),
- normalizeUrlString(extras.getString("Version")),
- normalizeUrlString(extras.getString("BuildID")));
- }
-
- /**
- * Launch the crash reporter activity that sends the crash report to the server.
- *
- * @param dumpFile Path for the minidump file
- * @param extraFile Path for the crash extra file
- * @return Whether the crash reporter was successfully launched
- */
- protected boolean launchCrashReporter(final String dumpFile, final String extraFile) {
- try {
- final Context context = getAppContext();
- final String javaPkg = getJavaPackageName();
- final String pkg = getAppPackageName();
- final String component = javaPkg + ".CrashReporter";
- final String action = javaPkg + ".reportCrash";
- final ProcessBuilder pb;
-
- if (context != null) {
- final Intent intent = new Intent(action);
- intent.setComponent(new ComponentName(pkg, component));
- intent.putExtra("minidumpPath", dumpFile);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- return true;
- }
-
- // Avoid AppConstants dependency for SDK version constants,
- // because CrashHandler could be used outside of Fennec code.
- if (Build.VERSION.SDK_INT < 17) {
- pb = new ProcessBuilder(
- "/system/bin/am", "start",
- "-a", action,
- "-n", pkg + '/' + component,
- "--es", "minidumpPath", dumpFile);
- } else {
- pb = new ProcessBuilder(
- "/system/bin/am", "start",
- "--user", /* USER_CURRENT_OR_SELF */ "-3",
- "-a", action,
- "-n", pkg + '/' + component,
- "--es", "minidumpPath", dumpFile);
- }
-
- pb.start().waitFor();
-
- } catch (final IOException e) {
- Log.e(LOGTAG, "Error launching crash reporter", e);
- return false;
-
- } catch (final InterruptedException e) {
- Log.i(LOGTAG, "Interrupted while waiting to launch crash reporter", e);
- // Fall-through
- }
- return true;
- }
-
- /**
- * Report an exception to Socorro.
- *
- * @param thread The exception thread
- * @param exc An exception
- * @return Whether the exception was successfully reported
- */
- protected boolean reportException(final Thread thread, final Throwable exc) {
- final Context context = getAppContext();
- final String id = UUID.randomUUID().toString();
-
- // Use the cache directory under the app directory to store crash files.
- final File dir;
- if (context != null) {
- dir = context.getCacheDir();
- } else {
- dir = new File("/data/data/" + getAppPackageName() + "/cache");
- }
-
- dir.mkdirs();
- if (!dir.exists()) {
- return false;
- }
-
- final File dmpFile = new File(dir, id + ".dmp");
- final File extraFile = new File(dir, id + ".extra");
-
- try {
- // Write out minidump file as binary.
-
- final byte[] minidump = getCrashDump(thread, exc);
- final FileOutputStream dmpStream = new FileOutputStream(dmpFile);
- try {
- dmpStream.write(minidump);
- } finally {
- dmpStream.close();
- }
-
- } catch (final IOException e) {
- Log.e(LOGTAG, "Error writing minidump file", e);
- return false;
- }
-
- try {
- // Write out crash extra file as text.
-
- final Bundle extras = getCrashExtras(thread, exc);
- final String url = getServerUrl(extras);
- extras.putString("ServerURL", url);
-
- final BufferedWriter extraWriter = new BufferedWriter(new FileWriter(extraFile));
- try {
- for (String key : extras.keySet()) {
- // Each extra line is in the format, key=value, with newlines escaped.
- extraWriter.write(key);
- extraWriter.write('=');
- extraWriter.write(String.valueOf(extras.get(key)).replace("\n", "\\n"));
- extraWriter.write('\n');
- }
- } finally {
- extraWriter.close();
- }
-
- } catch (final IOException e) {
- Log.e(LOGTAG, "Error writing extra file", e);
- return false;
- }
-
- return launchCrashReporter(dmpFile.getAbsolutePath(), extraFile.getAbsolutePath());
- }
-
- /**
- * Implements the default behavior for handling uncaught exceptions.
- *
- * @param thread The exception thread
- * @param exc An uncaught exception
- */
- @Override
- public void uncaughtException(Thread thread, Throwable exc) {
- if (this.crashing) {
- // Prevent possible infinite recusions.
- return;
- }
-
- if (thread == null) {
- // Gecko may pass in null for thread to denote the current thread.
- thread = Thread.currentThread();
- }
-
- try {
- if (!this.unregistered) {
- // Only process crash ourselves if we have not been unregistered.
-
- this.crashing = true;
- exc = getRootException(exc);
- logException(thread, exc);
-
- if (reportException(thread, exc)) {
- // Reporting succeeded; we can terminate our process now.
- return;
- }
- }
-
- if (systemUncaughtHandler != null) {
- // Follow the chain of uncaught handlers.
- systemUncaughtHandler.uncaughtException(thread, exc);
- }
- } finally {
- terminateProcess();
- }
- }
-
- public static CrashHandler createDefaultCrashHandler(final Context context) {
- return new CrashHandler(context) {
- @Override
- protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
- final Bundle extras = super.getCrashExtras(thread, exc);
-
- extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
- extras.putString("ProductID", AppConstants.MOZ_APP_ID);
- extras.putString("Version", AppConstants.MOZ_APP_VERSION);
- extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
- extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
- extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
- return extras;
- }
-
- @Override
- public boolean reportException(final Thread thread, final Throwable exc) {
- if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
- // Only use Java crash reporter if enabled on official build.
- return super.reportException(thread, exc);
- }
- return false;
- }
- };
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/EventDispatcher.java
+++ /dev/null
@@ -1,409 +0,0 @@
-/* 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.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.util.BundleEventListener;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSContainer;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-@RobocopTarget
-public final class EventDispatcher {
- private static final String LOGTAG = "GeckoEventDispatcher";
- private static final String GUID = "__guid__";
- private static final String STATUS_ERROR = "error";
- private static final String STATUS_SUCCESS = "success";
-
- private static final EventDispatcher INSTANCE = new EventDispatcher();
-
- /**
- * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size
- * of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to
- * empirically determine the initial capacity that avoids rehashing, we need to
- * determine the initial size, divide it by 75%, and round up to the next power-of-2.
- */
- private static final int DEFAULT_GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap
- private static final int DEFAULT_GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured
- private static final int DEFAULT_UI_EVENTS_COUNT = 0; // Default for HashMap
- private static final int DEFAULT_BACKGROUND_EVENTS_COUNT = 0; // Default for HashMap
-
- private final Map<String, List<NativeEventListener>> mGeckoThreadNativeListeners =
- new HashMap<String, List<NativeEventListener>>(DEFAULT_GECKO_NATIVE_EVENTS_COUNT);
- private final Map<String, List<GeckoEventListener>> mGeckoThreadJSONListeners =
- new HashMap<String, List<GeckoEventListener>>(DEFAULT_GECKO_JSON_EVENTS_COUNT);
- private final Map<String, List<BundleEventListener>> mUiThreadListeners =
- new HashMap<String, List<BundleEventListener>>(DEFAULT_UI_EVENTS_COUNT);
- private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners =
- new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT);
-
- public static EventDispatcher getInstance() {
- return INSTANCE;
- }
-
- private EventDispatcher() {
- }
-
- private <T> void registerListener(final Class<?> listType,
- final Map<String, List<T>> listenersMap,
- final T listener,
- final String[] events) {
- try {
- synchronized (listenersMap) {
- for (final String event : events) {
- List<T> listeners = listenersMap.get(event);
- if (listeners == null) {
- // Java doesn't let us put Class<? extends List<T>> as the type for listType.
- @SuppressWarnings("unchecked")
- final Class<? extends List<T>> type = (Class) listType;
- listeners = type.newInstance();
- listenersMap.put(event, listeners);
- }
- if (!AppConstants.RELEASE_BUILD && listeners.contains(listener)) {
- throw new IllegalStateException("Already registered " + event);
- }
- listeners.add(listener);
- }
- }
- } catch (final IllegalAccessException | InstantiationException e) {
- throw new IllegalArgumentException("Invalid new list type", e);
- }
- }
-
- private void checkNotRegisteredElsewhere(final Map<String, ?> allowedMap,
- final String[] events) {
- if (AppConstants.RELEASE_BUILD) {
- // for performance reasons, we only check for
- // already-registered listeners in non-release builds.
- return;
- }
- for (final Map<String, ?> listenersMap : Arrays.asList(mGeckoThreadNativeListeners,
- mGeckoThreadJSONListeners,
- mUiThreadListeners,
- mBackgroundThreadListeners)) {
- if (listenersMap == allowedMap) {
- continue;
- }
- synchronized (listenersMap) {
- for (final String event : events) {
- if (listenersMap.get(event) != null) {
- throw new IllegalStateException(
- "Already registered " + event + " under a different type");
- }
- }
- }
- }
- }
-
- private <T> void unregisterListener(final Map<String, List<T>> listenersMap,
- final T listener,
- final String[] events) {
- synchronized (listenersMap) {
- for (final String event : events) {
- List<T> listeners = listenersMap.get(event);
- if ((listeners == null ||
- !listeners.remove(listener)) && !AppConstants.RELEASE_BUILD) {
- throw new IllegalArgumentException(event + " was not registered");
- }
- }
- }
- }
-
- public void registerGeckoThreadListener(final NativeEventListener listener,
- final String... events) {
- checkNotRegisteredElsewhere(mGeckoThreadNativeListeners, events);
-
- // For listeners running on the Gecko thread, we want to notify the listeners
- // outside of our synchronized block, because the listeners may take an
- // indeterminate amount of time to run. Therefore, to ensure concurrency when
- // iterating the list outside of the synchronized block, we use a
- // CopyOnWriteArrayList.
- registerListener(CopyOnWriteArrayList.class,
- mGeckoThreadNativeListeners, listener, events);
- }
-
- @Deprecated // Use NativeEventListener instead
- public void registerGeckoThreadListener(final GeckoEventListener listener,
- final String... events) {
- checkNotRegisteredElsewhere(mGeckoThreadJSONListeners, events);
-
- registerListener(CopyOnWriteArrayList.class,
- mGeckoThreadJSONListeners, listener, events);
- }
-
- public void registerUiThreadListener(final BundleEventListener listener,
- final String... events) {
- checkNotRegisteredElsewhere(mUiThreadListeners, events);
-
- registerListener(ArrayList.class,
- mUiThreadListeners, listener, events);
- }
-
- public void registerBackgroundThreadListener(final BundleEventListener listener,
- final String... events) {
- checkNotRegisteredElsewhere(mBackgroundThreadListeners, events);
-
- registerListener(ArrayList.class,
- mBackgroundThreadListeners, listener, events);
- }
-
- public void unregisterGeckoThreadListener(final NativeEventListener listener,
- final String... events) {
- unregisterListener(mGeckoThreadNativeListeners, listener, events);
- }
-
- @Deprecated // Use NativeEventListener instead
- public void unregisterGeckoThreadListener(final GeckoEventListener listener,
- final String... events) {
- unregisterListener(mGeckoThreadJSONListeners, listener, events);
- }
-
- public void unregisterUiThreadListener(final BundleEventListener listener,
- final String... events) {
- unregisterListener(mUiThreadListeners, listener, events);
- }
-
- public void unregisterBackgroundThreadListener(final BundleEventListener listener,
- final String... events) {
- unregisterListener(mBackgroundThreadListeners, listener, events);
- }
-
- public void dispatchEvent(final NativeJSContainer message) {
- // First try native listeners.
- final String type = message.optString("type", null);
- if (type == null) {
- Log.e(LOGTAG, "JSON message must have a type property");
- return;
- }
-
- final List<NativeEventListener> listeners;
- synchronized (mGeckoThreadNativeListeners) {
- listeners = mGeckoThreadNativeListeners.get(type);
- }
-
- final String guid = message.optString(GUID, null);
- EventCallback callback = null;
- if (guid != null) {
- callback = new GeckoEventCallback(guid, type);
- }
-
- if (listeners != null) {
- if (listeners.isEmpty()) {
- Log.w(LOGTAG, "No listeners for " + type);
-
- // There were native listeners, and they're gone. Dispatch an error rather than
- // looking for JSON listeners.
- if (callback != null) {
- callback.sendError("No listeners for request");
- }
- }
- try {
- for (final NativeEventListener listener : listeners) {
- listener.handleMessage(type, message, callback);
- }
- } catch (final NativeJSObject.InvalidPropertyException e) {
- Log.e(LOGTAG, "Exception occurred while handling " + type, e);
- }
- // If we found native listeners, we assume we don't have any other types of listeners
- // and return early. This assumption is checked when registering listeners.
- return;
- }
-
- // Check for thread event listeners before checking for JSON event listeners,
- // because checking for thread listeners is very fast and doesn't require us to
- // serialize into JSON and construct a JSONObject.
- if (dispatchToThread(type, message, callback,
- mUiThreadListeners, ThreadUtils.getUiHandler()) ||
- dispatchToThread(type, message, callback,
- mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) {
-
- // If we found thread listeners, we assume we don't have any other types of listeners
- // and return early. This assumption is checked when registering listeners.
- return;
- }
-
- try {
- // If we didn't find native listeners, try JSON listeners.
- dispatchEvent(new JSONObject(message.toString()), callback);
- } catch (final JSONException e) {
- Log.e(LOGTAG, "Cannot parse JSON", e);
- } catch (final UnsupportedOperationException e) {
- Log.e(LOGTAG, "Cannot convert message to JSON", e);
- }
- }
-
- private boolean dispatchToThread(final String type,
- final NativeJSObject message,
- final EventCallback callback,
- final Map<String, List<BundleEventListener>> listenersMap,
- final Handler thread) {
- // We need to hold the lock throughout dispatching, to ensure the listeners list
- // is consistent, while we iterate over it. We don't have to worry about listeners
- // running for a long time while we have the lock, because the listeners will run
- // on a separate thread.
- synchronized (listenersMap) {
- final List<BundleEventListener> listeners = listenersMap.get(type);
- if (listeners == null) {
- return false;
- }
-
- if (listeners.isEmpty()) {
- Log.w(LOGTAG, "No listeners for " + type);
-
- // There were native listeners, and they're gone.
- // Dispatch an error rather than looking for more listeners.
- if (callback != null) {
- callback.sendError("No listeners for request");
- }
- return true;
- }
-
- final Bundle messageAsBundle;
- try {
- messageAsBundle = message.toBundle();
- } catch (final NativeJSObject.InvalidPropertyException e) {
- Log.e(LOGTAG, "Exception occurred while handling " + type, e);
- return true;
- }
-
- // Event listeners will call | callback.sendError | if applicable.
- for (final BundleEventListener listener : listeners) {
- thread.post(new Runnable() {
- @Override
- public void run() {
- listener.handleMessage(type, messageAsBundle, callback);
- }
- });
- }
- return true;
- }
- }
-
- public void dispatchEvent(final JSONObject message, final EventCallback callback) {
- // {
- // "type": "value",
- // "event_specific": "value",
- // ...
- try {
- final String type = message.getString("type");
-
- List<GeckoEventListener> listeners;
- synchronized (mGeckoThreadJSONListeners) {
- listeners = mGeckoThreadJSONListeners.get(type);
- }
- if (listeners == null || listeners.isEmpty()) {
- Log.w(LOGTAG, "No listeners for " + type);
-
- // If there are no listeners, dispatch an error.
- if (callback != null) {
- callback.sendError("No listeners for request");
- }
- return;
- }
- for (final GeckoEventListener listener : listeners) {
- listener.handleMessage(type, message);
- }
- } catch (final JSONException e) {
- Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
- }
- }
-
- @RobocopTarget
- @Deprecated
- public static void sendResponse(JSONObject message, Object response) {
- sendResponseHelper(STATUS_SUCCESS, message, response);
- }
-
- @Deprecated
- public static void sendError(JSONObject message, Object response) {
- sendResponseHelper(STATUS_ERROR, message, response);
- }
-
- @Deprecated
- private static void sendResponseHelper(String status, JSONObject message, Object response) {
- try {
- final String topic = message.getString("type") + ":Response";
- final JSONObject wrapper = new JSONObject();
- wrapper.put(GUID, message.getString(GUID));
- wrapper.put("status", status);
- wrapper.put("response", response);
-
- if (ThreadUtils.isOnGeckoThread()) {
- GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
- } else {
- GeckoAppShell.notifyObservers(topic, wrapper.toString(),
- GeckoThread.State.PROFILE_READY);
- }
- } catch (final JSONException e) {
- Log.e(LOGTAG, "Unable to send response", e);
- }
- }
-
- private static class GeckoEventCallback implements EventCallback {
- private final String guid;
- private final String type;
- private boolean sent;
-
- public GeckoEventCallback(final String guid, final String type) {
- this.guid = guid;
- this.type = type;
- }
-
- @Override
- public void sendSuccess(final Object response) {
- sendResponse(STATUS_SUCCESS, response);
- }
-
- @Override
- public void sendError(final Object response) {
- sendResponse(STATUS_ERROR, response);
- }
-
- private void sendResponse(final String status, final Object response) {
- if (sent) {
- throw new IllegalStateException("Callback has already been executed for type=" +
- type + ", guid=" + guid);
- }
-
- sent = true;
-
- try {
- final String topic = type + ":Response";
- final JSONObject wrapper = new JSONObject();
- wrapper.put(GUID, guid);
- wrapper.put("status", status);
- wrapper.put("response", response);
-
- if (ThreadUtils.isOnGeckoThread()) {
- GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
- } else {
- GeckoAppShell.notifyObservers(topic, wrapper.toString(),
- GeckoThread.State.PROFILE_READY);
- }
- } catch (final JSONException e) {
- Log.e(LOGTAG, "Unable to send response for: " + type, e);
- }
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeProvider;
-
-import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
-import com.googlecode.eyesfree.braille.selfbraille.WriteData;
-
-public class GeckoAccessibility {
- private static final String LOGTAG = "GeckoAccessibility";
- private static final int VIRTUAL_ENTRY_POINT_BEFORE = 1;
- private static final int VIRTUAL_CURSOR_POSITION = 2;
- private static final int VIRTUAL_ENTRY_POINT_AFTER = 3;
-
- private static boolean sEnabled;
- // Used to store the JSON message and populate the event later in the code path.
- private static JSONObject sHoverEnter;
- private static AccessibilityNodeInfo sVirtualCursorNode;
- private static int sCurrentNode;
-
- // This is the number Brailleback uses to start indexing routing keys.
- private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
- private static SelfBrailleClient sSelfBrailleClient;
-
- public static void updateAccessibilitySettings (final Context context) {
- new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
- @Override
- public Void doInBackground() {
- JSONObject ret = new JSONObject();
- sEnabled = false;
- AccessibilityManager accessibilityManager =
- (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
- sEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();
- if (Versions.feature16Plus && sEnabled && sSelfBrailleClient == null) {
- sSelfBrailleClient = new SelfBrailleClient(GeckoAppShell.getContext(), false);
- }
-
- try {
- ret.put("enabled", sEnabled);
- } catch (Exception ex) {
- Log.e(LOGTAG, "Error building JSON arguments for Accessibility:Settings:", ex);
- }
-
- GeckoAppShell.notifyObservers("Accessibility:Settings", ret.toString());
- return null;
- }
-
- @Override
- public void onPostExecute(Void args) {
- final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
- if (geckoInterface == null) {
- return;
- }
- geckoInterface.setAccessibilityEnabled(sEnabled);
- }
- }.execute();
- }
-
- private static void populateEventFromJSON (AccessibilityEvent event, JSONObject message) {
- final JSONArray textArray = message.optJSONArray("text");
- if (textArray != null) {
- for (int i = 0; i < textArray.length(); i++)
- event.getText().add(textArray.optString(i));
- }
-
- event.setContentDescription(message.optString("description"));
- event.setEnabled(message.optBoolean("enabled", true));
- event.setChecked(message.optBoolean("checked"));
- event.setPassword(message.optBoolean("password"));
- event.setAddedCount(message.optInt("addedCount", -1));
- event.setRemovedCount(message.optInt("removedCount", -1));
- event.setFromIndex(message.optInt("fromIndex", -1));
- event.setItemCount(message.optInt("itemCount", -1));
- event.setCurrentItemIndex(message.optInt("currentItemIndex", -1));
- event.setBeforeText(message.optString("beforeText"));
- if (Versions.feature14Plus) {
- event.setToIndex(message.optInt("toIndex", -1));
- event.setScrollable(message.optBoolean("scrollable"));
- event.setScrollX(message.optInt("scrollX", -1));
- event.setScrollY(message.optInt("scrollY", -1));
- }
- if (Versions.feature15Plus) {
- event.setMaxScrollX(message.optInt("maxScrollX", -1));
- event.setMaxScrollY(message.optInt("maxScrollY", -1));
- }
- }
-
- private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
- final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
- accEvent.setClassName(GeckoAccessibility.class.getName());
- accEvent.setPackageName(GeckoAppShell.getContext().getPackageName());
- populateEventFromJSON(accEvent, message);
- AccessibilityManager accessibilityManager =
- (AccessibilityManager) GeckoAppShell.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
- try {
- accessibilityManager.sendAccessibilityEvent(accEvent);
- } catch (IllegalStateException e) {
- // Accessibility is off.
- }
- }
-
- public static boolean isEnabled() {
- return sEnabled;
- }
-
- public static void sendAccessibilityEvent (final JSONObject message) {
- if (!sEnabled)
- return;
-
- final int eventType = message.optInt("eventType", -1);
- if (eventType < 0) {
- Log.e(LOGTAG, "No accessibility event type provided");
- return;
- }
-
- sendAccessibilityEvent(message, eventType);
- }
-
- public static void sendAccessibilityEvent (final JSONObject message, final int eventType) {
- if (!sEnabled)
- return;
-
- final String exitView = message.optString("exitView");
- if (exitView.equals("moveNext")) {
- sCurrentNode = VIRTUAL_ENTRY_POINT_AFTER;
- } else if (exitView.equals("movePrevious")) {
- sCurrentNode = VIRTUAL_ENTRY_POINT_BEFORE;
- } else {
- sCurrentNode = VIRTUAL_CURSOR_POSITION;
- }
-
- if (Versions.preJB) {
- // Before Jelly Bean we send events directly from here while spoofing the source by setting
- // the package and class name manually.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- sendDirectAccessibilityEvent(eventType, message);
- }
- });
- } else {
- // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
- // it work with TalkBack.
- final LayerView view = GeckoAppShell.getLayerView();
- if (view == null)
- return;
-
- if (sVirtualCursorNode == null)
- sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
- sVirtualCursorNode.setEnabled(message.optBoolean("enabled", true));
- sVirtualCursorNode.setClickable(message.optBoolean("clickable"));
- sVirtualCursorNode.setCheckable(message.optBoolean("checkable"));
- sVirtualCursorNode.setChecked(message.optBoolean("checked"));
- sVirtualCursorNode.setPassword(message.optBoolean("password"));
-
- final JSONArray textArray = message.optJSONArray("text");
- StringBuilder sb = new StringBuilder();
- if (textArray != null && textArray.length() > 0) {
- sb.append(textArray.optString(0));
- for (int i = 1; i < textArray.length(); i++) {
- sb.append(" ").append(textArray.optString(i));
- }
- sVirtualCursorNode.setText(sb.toString());
- }
- sVirtualCursorNode.setContentDescription(message.optString("description"));
-
- JSONObject bounds = message.optJSONObject("bounds");
- if (bounds != null) {
- Rect relativeBounds = new Rect(bounds.optInt("left"), bounds.optInt("top"),
- bounds.optInt("right"), bounds.optInt("bottom"));
- sVirtualCursorNode.setBoundsInParent(relativeBounds);
- int[] locationOnScreen = new int[2];
- view.getLocationOnScreen(locationOnScreen);
- Rect screenBounds = new Rect(relativeBounds);
- screenBounds.offset(locationOnScreen[0], locationOnScreen[1]);
- sVirtualCursorNode.setBoundsInScreen(screenBounds);
- }
-
- final JSONObject braille = message.optJSONObject("brailleOutput");
- if (braille != null) {
- sendBrailleText(view, braille.optString("text"),
- braille.optInt("selectionStart"), braille.optInt("selectionEnd"));
- }
-
- if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
- sHoverEnter = message;
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
- event.setPackageName(GeckoAppShell.getContext().getPackageName());
- event.setClassName(GeckoAccessibility.class.getName());
- if (eventType == AccessibilityEvent.TYPE_ANNOUNCEMENT ||
- eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
- event.setSource(view, View.NO_ID);
- } else {
- event.setSource(view, VIRTUAL_CURSOR_POSITION);
- }
- populateEventFromJSON(event, message);
- view.requestSendAccessibilityEvent(view, event);
- }
- });
-
- }
- }
-
- private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
- AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
- WriteData data = WriteData.forInfo(info);
- data.setText(text);
- // Set either the focus blink or the current caret position/selection
- data.setSelectionStart(selectionStart);
- data.setSelectionEnd(selectionEnd);
- sSelfBrailleClient.write(data);
- }
-
- public static void setDelegate(LayerView layerview) {
- // Only use this delegate in Jelly Bean.
- if (Versions.feature16Plus) {
- layerview.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
- layerview.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- }
- }
-
- public static void setAccessibilityManagerListeners(final Context context) {
- AccessibilityManager accessibilityManager =
- (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
-
- accessibilityManager.addAccessibilityStateChangeListener(new AccessibilityManager.AccessibilityStateChangeListener() {
- @Override
- public void onAccessibilityStateChanged(boolean enabled) {
- updateAccessibilitySettings(context);
- }
- });
-
- if (Versions.feature19Plus) {
- accessibilityManager.addTouchExplorationStateChangeListener(new AccessibilityManager.TouchExplorationStateChangeListener() {
- @Override
- public void onTouchExplorationStateChanged(boolean enabled) {
- updateAccessibilitySettings(context);
- }
- });
- }
- }
-
- public static void onLayerViewFocusChanged(LayerView layerview, boolean gainFocus) {
- if (sEnabled)
- GeckoAppShell.notifyObservers("Accessibility:Focus", gainFocus ? "true" : "false");
- }
-
- public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
- AccessibilityNodeProvider mAccessibilityNodeProvider;
-
- @Override
- public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
- if (mAccessibilityNodeProvider == null)
- // The accessibility node structure for web content consists of 3 LayerView child nodes:
- // 1. VIRTUAL_ENTRY_POINT_BEFORE: Represents the entry point before the LayerView.
- // 2. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
- // 3. VIRTUAL_ENTRY_POINT_AFTER: Represents the entry point after the LayerView.
- mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
- @Override
- public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
- AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CURSOR_POSITION && sVirtualCursorNode != null) ?
- AccessibilityNodeInfo.obtain(sVirtualCursorNode) :
- AccessibilityNodeInfo.obtain(host, virtualDescendantId);
-
- switch (virtualDescendantId) {
- case View.NO_ID:
- // This is the parent LayerView node, populate it with children.
- onInitializeAccessibilityNodeInfo(host, info);
- info.addChild(host, VIRTUAL_ENTRY_POINT_BEFORE);
- info.addChild(host, VIRTUAL_CURSOR_POSITION);
- info.addChild(host, VIRTUAL_ENTRY_POINT_AFTER);
- break;
- default:
- info.setParent(host);
- info.setSource(host, virtualDescendantId);
- info.setVisibleToUser(host.isShown());
- info.setPackageName(GeckoAppShell.getContext().getPackageName());
- info.setClassName(host.getClass().getName());
- info.setEnabled(true);
- info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
- info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
- info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
- info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
- info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
- info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
- info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
- info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
- info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
- info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
- info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
- AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
- AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
- AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
- break;
- }
- return info;
- }
-
- @Override
- public boolean performAction (int virtualViewId, int action, Bundle arguments) {
- if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
- // The accessibility focus is permanently on the middle node, VIRTUAL_CURSOR_POSITION.
- // When we enter the view forward or backward we just ask Gecko to get focus, keeping the current position.
- if (virtualViewId == VIRTUAL_CURSOR_POSITION && sHoverEnter != null) {
- GeckoAccessibility.sendAccessibilityEvent(sHoverEnter, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
- } else {
- GeckoAppShell.notifyObservers("Accessibility:Focus", "true");
- }
- return true;
- } else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
- GeckoAppShell.notifyObservers("Accessibility:ActivateObject", null);
- return true;
- } else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
- GeckoAppShell.notifyObservers("Accessibility:LongPress", null);
- return true;
- } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
- GeckoAppShell.notifyObservers("Accessibility:ScrollForward", null);
- return true;
- } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
- GeckoAppShell.notifyObservers("Accessibility:ScrollBackward", null);
- return true;
- } else if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
- String traversalRule = "";
- if (arguments != null) {
- traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
- }
- GeckoAppShell.notifyObservers("Accessibility:NextObject", traversalRule);
- return true;
- } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
- String traversalRule = "";
- if (arguments != null) {
- traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
- }
- GeckoAppShell.notifyObservers("Accessibility:PreviousObject", traversalRule);
- return true;
- } else if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY &&
- virtualViewId == VIRTUAL_CURSOR_POSITION) {
- // XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
- // the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
- // Other negative values are used by ChromeVox, but we don't support them.
- // FAKE_GRANULARITY_READ_CURRENT = -1
- // FAKE_GRANULARITY_READ_TITLE = -2
- // FAKE_GRANULARITY_STOP_SPEECH = -3
- // FAKE_GRANULARITY_CHANGE_SHIFTER = -4
- int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
- if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
- int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
- JSONObject activationData = new JSONObject();
- try {
- activationData.put("keyIndex", keyIndex);
- } catch (JSONException e) {
- return true;
- }
- GeckoAppShell.notifyObservers("Accessibility:ActivateObject", activationData.toString());
- } else if (granularity > 0) {
- JSONObject movementData = new JSONObject();
- try {
- movementData.put("direction", "Next");
- movementData.put("granularity", granularity);
- } catch (JSONException e) {
- return true;
- }
- GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
- }
- return true;
- } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY &&
- virtualViewId == VIRTUAL_CURSOR_POSITION) {
- JSONObject movementData = new JSONObject();
- int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
- try {
- movementData.put("direction", "Previous");
- movementData.put("granularity", granularity);
- } catch (JSONException e) {
- return true;
- }
- if (granularity > 0) {
- GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
- }
- return true;
- }
- return host.performAccessibilityAction(action, arguments);
- }
- };
-
- return mAccessibilityNodeProvider;
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ /dev/null
@@ -1,2362 +0,0 @@
-/* -*- 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.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
-import java.net.MalformedURLException;
-import java.net.Proxy;
-import java.net.URLConnection;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.StringTokenizer;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
-
-import android.annotation.SuppressLint;
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.gfx.PanZoomController;
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.GeckoRequest;
-import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSContainer;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ProxySelector;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.Manifest;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.pm.Signature;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.ImageFormat;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.SurfaceTexture;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.hardware.Sensor;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.location.Criteria;
-import android.location.Location;
-import android.location.LocationListener;
-import android.location.LocationManager;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Looper;
-import android.os.Vibrator;
-import android.provider.Settings;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.ContextThemeWrapper;
-import android.view.Display;
-import android.view.HapticFeedbackConstants;
-import android.view.Surface;
-import android.view.SurfaceView;
-import android.view.TextureView;
-import android.view.View;
-import android.view.WindowManager;
-import android.webkit.MimeTypeMap;
-import android.widget.AbsoluteLayout;
-
-public class GeckoAppShell
-{
- private static final String LOGTAG = "GeckoAppShell";
-
- // We have static members only.
- private GeckoAppShell() { }
-
- public static final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
- public static final String ACTION_HOMESCREEN_SHORTCUT = "org.mozilla.gecko.BOOKMARK";
- public static final String PREFS_OOM_EXCEPTION = "OOMException";
-
- private static final CrashHandler CRASH_HANDLER = new CrashHandler() {
- @Override
- protected String getAppPackageName() {
- return AppConstants.ANDROID_PACKAGE_NAME;
- }
-
- @Override
- protected Context getAppContext() {
- return sContextGetter != null ? getApplicationContext() : null;
- }
-
- @Override
- protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
- final Bundle extras = super.getCrashExtras(thread, exc);
-
- extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
- extras.putString("ProductID", AppConstants.MOZ_APP_ID);
- extras.putString("Version", AppConstants.MOZ_APP_VERSION);
- extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
- extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
- extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
- return extras;
- }
-
- @Override
- public void uncaughtException(final Thread thread, final Throwable exc) {
- if (GeckoThread.isState(GeckoThread.State.EXITING) ||
- GeckoThread.isState(GeckoThread.State.EXITED)) {
- // We've called System.exit. All exceptions after this point are Android
- // berating us for being nasty to it.
- return;
- }
-
- super.uncaughtException(thread, exc);
- }
-
- @Override
- public boolean reportException(final Thread thread, final Throwable exc) {
- try {
- if (exc instanceof OutOfMemoryError) {
- SharedPreferences prefs = getSharedPreferences();
- SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean(PREFS_OOM_EXCEPTION, true);
-
- // Synchronously write to disk so we know it's done before we
- // shutdown
- editor.commit();
- }
-
- reportJavaCrash(getExceptionStackTrace(exc));
-
- } catch (final Throwable e) {
- }
-
- // reportJavaCrash should have caused us to hard crash. If we're still here,
- // it probably means Gecko is not loaded, and we should do something else.
- if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
- // Only use Java crash reporter if enabled on official build.
- return super.reportException(thread, exc);
- }
- return false;
- }
- };
-
- public static CrashHandler ensureCrashHandling() {
- // Crash handling is automatically enabled when GeckoAppShell is loaded.
- return CRASH_HANDLER;
- }
-
- private static final Map<String, String> ALERT_COOKIES = new ConcurrentHashMap<String, String>();
-
- private static volatile boolean locationHighAccuracyEnabled;
-
- // Accessed by NotificationHelper. This should be encapsulated.
- /* package */ static NotificationClient notificationClient;
-
- // See also HardwareUtils.LOW_MEMORY_THRESHOLD_MB.
- private static final int HIGH_MEMORY_DEVICE_THRESHOLD_MB = 768;
-
- static private int sDensityDpi;
- static private int sScreenDepth;
-
- /* Is the value in sVibrationEndTime valid? */
- private static boolean sVibrationMaybePlaying;
-
- /* Time (in System.nanoTime() units) when the currently-playing vibration
- * is scheduled to end. This value is valid only when
- * sVibrationMaybePlaying is true. */
- private static long sVibrationEndTime;
-
- private static Sensor gAccelerometerSensor;
- private static Sensor gLinearAccelerometerSensor;
- private static Sensor gGyroscopeSensor;
- private static Sensor gOrientationSensor;
- private static Sensor gProximitySensor;
- private static Sensor gLightSensor;
- private static Sensor gRotationVectorSensor;
- private static Sensor gGameRotationVectorSensor;
-
- private static final String GECKOREQUEST_RESPONSE_KEY = "response";
- private static final String GECKOREQUEST_ERROR_KEY = "error";
-
- /*
- * Keep in sync with constants found here:
- * http://mxr.mozilla.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl
- */
- static public final int WPL_STATE_START = 0x00000001;
- static public final int WPL_STATE_STOP = 0x00000010;
- static public final int WPL_STATE_IS_DOCUMENT = 0x00020000;
- static public final int WPL_STATE_IS_NETWORK = 0x00040000;
-
- /* Keep in sync with constants found here:
- http://mxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
- */
- static public final int LINK_TYPE_UNKNOWN = 0;
- static public final int LINK_TYPE_ETHERNET = 1;
- static public final int LINK_TYPE_USB = 2;
- static public final int LINK_TYPE_WIFI = 3;
- static public final int LINK_TYPE_WIMAX = 4;
- static public final int LINK_TYPE_2G = 5;
- static public final int LINK_TYPE_3G = 6;
- static public final int LINK_TYPE_4G = 7;
-
- /* The Android-side API: API methods that Android calls */
-
- // Initialization methods
- public static native void registerJavaUiThread();
-
- // helper methods
- public static void callObserver(String observerKey, String topic, String data) {
- sendEventToGecko(GeckoEvent.createCallObserverEvent(observerKey, topic, data));
- }
- public static void removeObserver(String observerKey) {
- sendEventToGecko(GeckoEvent.createRemoveObserverEvent(observerKey));
- }
- public static native void onSurfaceTextureFrameAvailable(Object surfaceTexture, int id);
- public static native void dispatchMemoryPressure();
-
- private static native void reportJavaCrash(String stackTrace);
-
- public static void notifyUriVisited(String uri) {
- sendEventToGecko(GeckoEvent.createVisitedEvent(uri));
- }
-
- public static native void notifyBatteryChange(double aLevel, boolean aCharging, double aRemainingTime);
-
- public static native void invalidateAndScheduleComposite();
-
- public static native float computeRenderIntegrity();
-
- public static native SurfaceBits getSurfaceBits(Surface surface);
-
- public static native void addPresentationSurface(Surface surface);
- public static native void removePresentationSurface(Surface surface);
-
- public static native void onFullScreenPluginHidden(View view);
-
- private static LayerView sLayerView;
- private static Rect sScreenSize;
-
- public static void setLayerView(LayerView lv) {
- if (sLayerView == lv) {
- return;
- }
- sLayerView = lv;
- }
-
- @RobocopTarget
- public static LayerView getLayerView() {
- return sLayerView;
- }
-
- /**
- * If the Gecko thread is running, immediately dispatches the event to
- * Gecko.
- *
- * If the Gecko thread is not running, queues the event. If the queue is
- * full, throws {@link IllegalStateException}.
- *
- * Queued events will be dispatched in order of arrival when the Gecko
- * thread becomes live.
- *
- * This method can be called from any thread.
- *
- * @param e
- * the event to dispatch. Cannot be null.
- */
- @RobocopTarget
- public static void sendEventToGecko(GeckoEvent e) {
- if (e == null) {
- throw new IllegalArgumentException("e cannot be null.");
- }
-
- if (GeckoThread.isRunning()) {
- notifyGeckoOfEvent(e);
- // Gecko will copy the event data into a normal C++ object.
- // We can recycle the event now.
- e.recycle();
- return;
- }
-
- GeckoThread.addPendingEvent(e);
- }
-
- /**
- * Sends an asynchronous request to Gecko.
- *
- * The response data will be passed to {@link GeckoRequest#onResponse(NativeJSObject)} if the
- * request succeeds; otherwise, {@link GeckoRequest#onError()} will fire.
- *
- * This method follows the same queuing conditions as {@link #sendEventToGecko(GeckoEvent)}.
- * It can be called from any thread. The GeckoRequest callbacks will be executed on the Gecko thread.
- *
- * @param request The request to dispatch. Cannot be null.
- */
- @RobocopTarget
- public static void sendRequestToGecko(final GeckoRequest request) {
- final String responseMessage = "Gecko:Request" + request.getId();
-
- EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() {
- @Override
- public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
- EventDispatcher.getInstance().unregisterGeckoThreadListener(this, event);
- if (!message.has(GECKOREQUEST_RESPONSE_KEY)) {
- request.onError(message.getObject(GECKOREQUEST_ERROR_KEY));
- return;
- }
- request.onResponse(message.getObject(GECKOREQUEST_RESPONSE_KEY));
- }
- }, responseMessage);
-
- notifyObservers(request.getName(), request.getData());
- }
-
- // Tell the Gecko event loop that an event is available.
- public static native void notifyGeckoOfEvent(GeckoEvent event);
-
- // Synchronously notify a Gecko observer; must be called from Gecko thread.
- @WrapForJNI
- public static native void syncNotifyObservers(String topic, String data);
-
- @WrapForJNI(stubName = "NotifyObservers")
- private static native void nativeNotifyObservers(String topic, String data);
-
- @RobocopTarget
- public static void notifyObservers(final String topic, final String data) {
- notifyObservers(topic, data, GeckoThread.State.RUNNING);
- }
-
- public static void notifyObservers(final String topic, final String data, final GeckoThread.State state) {
- if (GeckoThread.isStateAtLeast(state)) {
- nativeNotifyObservers(topic, data);
- } else {
- GeckoThread.queueNativeCallUntil(
- state, GeckoAppShell.class, "nativeNotifyObservers",
- String.class, topic, String.class, data);
- }
- }
-
- /*
- * The Gecko-side API: API methods that Gecko calls
- */
-
- @WrapForJNI(allowMultithread = true, noThrow = true)
- public static String handleUncaughtException(Throwable e) {
- if (AppConstants.MOZ_CRASHREPORTER) {
- final Throwable exc = CrashHandler.getRootException(e);
- final StackTraceElement[] stack = exc.getStackTrace();
- if (stack.length >= 1 && stack[0].isNativeMethod()) {
- // The exception occurred when running native code. Return an exception
- // string and trigger the crash reporter inside the caller so that we get
- // a better native stack in Socorro.
- CrashHandler.logException(Thread.currentThread(), exc);
- return CrashHandler.getExceptionStackTrace(exc);
- }
- }
- CRASH_HANDLER.uncaughtException(null, e);
- return null;
- }
-
- private static final Runnable sCallbackRunnable = new Runnable() {
- @Override
- public void run() {
- ThreadUtils.assertOnUiThread();
- long nextDelay = runUiThreadCallback();
- if (nextDelay >= 0) {
- ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
- }
- }
- };
-
- private static native long runUiThreadCallback();
-
- @WrapForJNI(allowMultithread = true)
- private static void requestUiThreadCallback(long delay) {
- ThreadUtils.getUiHandler().postDelayed(sCallbackRunnable, delay);
- }
-
- private static float getLocationAccuracy(Location location) {
- float radius = location.getAccuracy();
- return (location.hasAccuracy() && radius > 0) ? radius : 1001;
- }
-
- @SuppressLint("MissingPermission") // Permissions are explicitly checked for in enableLocation()
- private static Location getLastKnownLocation(LocationManager lm) {
- Location lastKnownLocation = null;
- List<String> providers = lm.getAllProviders();
-
- for (String provider : providers) {
- Location location = lm.getLastKnownLocation(provider);
- if (location == null) {
- continue;
- }
-
- if (lastKnownLocation == null) {
- lastKnownLocation = location;
- continue;
- }
-
- long timeDiff = location.getTime() - lastKnownLocation.getTime();
- if (timeDiff > 0 ||
- (timeDiff == 0 &&
- getLocationAccuracy(location) < getLocationAccuracy(lastKnownLocation))) {
- lastKnownLocation = location;
- }
- }
-
- return lastKnownLocation;
- }
-
- @WrapForJNI
- @SuppressLint("MissingPermission") // Permissions are explicitly checked for within this method
- public static void enableLocation(final boolean enable) {
- Permissions
- .from((Activity) getContext())
- .withPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
- .onUIThread()
- .doNotPromptIf(!enable)
- .run(new Runnable() {
- @Override
- public void run() {
- LocationManager lm = getLocationManager(getApplicationContext());
- if (lm == null) {
- return;
- }
-
- if (enable) {
- Location lastKnownLocation = getLastKnownLocation(lm);
- if (lastKnownLocation != null) {
- getGeckoInterface().getLocationListener().onLocationChanged(lastKnownLocation);
- }
-
- Criteria criteria = new Criteria();
- criteria.setSpeedRequired(false);
- criteria.setBearingRequired(false);
- criteria.setAltitudeRequired(false);
- if (locationHighAccuracyEnabled) {
- criteria.setAccuracy(Criteria.ACCURACY_FINE);
- criteria.setCostAllowed(true);
- criteria.setPowerRequirement(Criteria.POWER_HIGH);
- } else {
- criteria.setAccuracy(Criteria.ACCURACY_COARSE);
- criteria.setCostAllowed(false);
- criteria.setPowerRequirement(Criteria.POWER_LOW);
- }
-
- String provider = lm.getBestProvider(criteria, true);
- if (provider == null)
- return;
-
- Looper l = Looper.getMainLooper();
- lm.requestLocationUpdates(provider, 100, (float) .5, getGeckoInterface().getLocationListener(), l);
- } else {
- lm.removeUpdates(getGeckoInterface().getLocationListener());
- }
- }
- });
- }
-
- private static LocationManager getLocationManager(Context context) {
- try {
- return (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
- } catch (NoSuchFieldError e) {
- // Some Tegras throw exceptions about missing the CONTROL_LOCATION_UPDATES permission,
- // which allows enabling/disabling location update notifications from the cell radio.
- // CONTROL_LOCATION_UPDATES is not for use by normal applications, but we might be
- // hitting this problem if the Tegras are confused about missing cell radios.
- Log.e(LOGTAG, "LOCATION_SERVICE not found?!", e);
- return null;
- }
- }
-
- @WrapForJNI
- public static void enableLocationHighAccuracy(final boolean enable) {
- locationHighAccuracyEnabled = enable;
- }
-
- @WrapForJNI
- public static boolean setAlarm(int aSeconds, int aNanoSeconds) {
- AlarmManager am = (AlarmManager)
- getApplicationContext().getSystemService(Context.ALARM_SERVICE);
-
- Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
- PendingIntent pi = PendingIntent.getBroadcast(
- getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- // AlarmManager only supports millisecond precision
- long time = ((long) aSeconds * 1000) + ((long) aNanoSeconds / 1_000_000L);
- am.setExact(AlarmManager.RTC_WAKEUP, time, pi);
-
- return true;
- }
-
- @WrapForJNI
- public static void disableAlarm() {
- AlarmManager am = (AlarmManager)
- getApplicationContext().getSystemService(Context.ALARM_SERVICE);
-
- Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
- PendingIntent pi = PendingIntent.getBroadcast(
- getApplicationContext(), 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- am.cancel(pi);
- }
-
- @WrapForJNI
- public static void enableSensor(int aSensortype) {
- GeckoInterface gi = getGeckoInterface();
- if (gi == null) {
- return;
- }
- SensorManager sm = (SensorManager)
- getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
-
- switch (aSensortype) {
- case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
- if (gGameRotationVectorSensor == null) {
- gGameRotationVectorSensor = sm.getDefaultSensor(15);
- // sm.getDefaultSensor(
- // Sensor.TYPE_GAME_ROTATION_VECTOR); // API >= 18
- }
- if (gGameRotationVectorSensor != null) {
- sm.registerListener(gi.getSensorEventListener(),
- gGameRotationVectorSensor,
- SensorManager.SENSOR_DELAY_FASTEST);
- }
- if (gGameRotationVectorSensor != null) {
- break;
- }
- // Fallthrough
-
- case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
- if (gRotationVectorSensor == null) {
- gRotationVectorSensor = sm.getDefaultSensor(
- Sensor.TYPE_ROTATION_VECTOR);
- }
- if (gRotationVectorSensor != null) {
- sm.registerListener(gi.getSensorEventListener(),
- gRotationVectorSensor,
- SensorManager.SENSOR_DELAY_FASTEST);
- }
- if (gRotationVectorSensor != null) {
- break;
- }
- // Fallthrough
-
- case GeckoHalDefines.SENSOR_ORIENTATION:
- if (gOrientationSensor == null) {
- gOrientationSensor = sm.getDefaultSensor(
- Sensor.TYPE_ORIENTATION);
- }
- if (gOrientationSensor != null) {
- sm.registerListener(gi.getSensorEventListener(),
- gOrientationSensor,
- SensorManager.SENSOR_DELAY_FASTEST);
- }
- break;
-
- case GeckoHalDefines.SENSOR_ACCELERATION:
- if (gAccelerometerSensor == null) {
- gAccelerometerSensor = sm.getDefaultSensor(
- Sensor.TYPE_ACCELEROMETER);
- }
- if (gAccelerometerSensor != null) {
- sm.registerListener(gi.getSensorEventListener(),
- gAccelerometerSensor,
- SensorManager.SENSOR_DELAY_FASTEST);
- }
- break;
-
- case GeckoHalDefines.SENSOR_PROXIMITY:
- if (gProximitySensor == null) {
- gProximitySensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
- }
- if (gProximitySensor != null) {
- sm.registerListener(gi.getSensorEventListener(),
- gProximitySensor,
- SensorManager.SENSOR_DELAY_NORMAL);
- }
- break;
-
- case GeckoHalDefines.SENSOR_LIGHT:
- if (gLightSensor == null) {
- gLightSensor = sm.getDefaultSensor(Sensor.TYPE_LIGHT);
- }
- if (gLightSensor != null) {
- sm.registerListener(gi.getSensorEventListener(),
- gLightSensor,
- SensorManager.SENSOR_DELAY_NORMAL);
- }
- break;
-
- case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
- if (gLinearAccelerometerSensor == null) {
- gLinearAccelerometerSensor = sm.getDefaultSensor(
- Sensor.TYPE_LINEAR_ACCELERATION);
- }
- if (gLinearAccelerometerSensor != null) {
- sm.registerListener(gi.getSensorEventListener(),
- gLinearAccelerometerSensor,
- SensorManager.SENSOR_DELAY_FASTEST);
- }
- break;
-
- case GeckoHalDefines.SENSOR_GYROSCOPE:
- if (gGyroscopeSensor == null) {
- gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
- }
- if (gGyroscopeSensor != null) {
- sm.registerListener(gi.getSensorEventListener(),
- gGyroscopeSensor,
- SensorManager.SENSOR_DELAY_FASTEST);
- }
- break;
-
- default:
- Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " +
- aSensortype);
- }
- }
-
- @WrapForJNI
- public static void disableSensor(int aSensortype) {
- GeckoInterface gi = getGeckoInterface();
- if (gi == null)
- return;
-
- SensorManager sm = (SensorManager)
- getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
-
- switch (aSensortype) {
- case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
- if (gGameRotationVectorSensor != null) {
- sm.unregisterListener(gi.getSensorEventListener(), gGameRotationVectorSensor);
- break;
- }
- // Fallthrough
-
- case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
- if (gRotationVectorSensor != null) {
- sm.unregisterListener(gi.getSensorEventListener(), gRotationVectorSensor);
- break;
- }
- // Fallthrough
-
- case GeckoHalDefines.SENSOR_ORIENTATION:
- if (gOrientationSensor != null) {
- sm.unregisterListener(gi.getSensorEventListener(), gOrientationSensor);
- }
- break;
-
- case GeckoHalDefines.SENSOR_ACCELERATION:
- if (gAccelerometerSensor != null) {
- sm.unregisterListener(gi.getSensorEventListener(), gAccelerometerSensor);
- }
- break;
-
- case GeckoHalDefines.SENSOR_PROXIMITY:
- if (gProximitySensor != null) {
- sm.unregisterListener(gi.getSensorEventListener(), gProximitySensor);
- }
- break;
-
- case GeckoHalDefines.SENSOR_LIGHT:
- if (gLightSensor != null) {
- sm.unregisterListener(gi.getSensorEventListener(), gLightSensor);
- }
- break;
-
- case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
- if (gLinearAccelerometerSensor != null) {
- sm.unregisterListener(gi.getSensorEventListener(), gLinearAccelerometerSensor);
- }
- break;
-
- case GeckoHalDefines.SENSOR_GYROSCOPE:
- if (gGyroscopeSensor != null) {
- sm.unregisterListener(gi.getSensorEventListener(), gGyroscopeSensor);
- }
- break;
- default:
- Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype);
- }
- }
-
- @WrapForJNI
- public static void startMonitoringGamepad() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- AndroidGamepadManager.startup();
- }
- });
- }
-
- @WrapForJNI
- public static void stopMonitoringGamepad() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- AndroidGamepadManager.shutdown();
- }
- });
- }
-
- @WrapForJNI
- public static void gamepadAdded(final int device_id, final int service_id) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- AndroidGamepadManager.gamepadAdded(device_id, service_id);
- }
- });
- }
-
- @WrapForJNI
- public static void moveTaskToBack() {
- if (getGeckoInterface() != null)
- getGeckoInterface().getActivity().moveTaskToBack(true);
- }
-
- @WrapForJNI
- static void scheduleRestart() {
- getGeckoInterface().doRestart();
- }
-
- // Creates a homescreen shortcut for a web page.
- // This is the entry point from nsIShellService.
- @WrapForJNI
- public static void createShortcut(final String aTitle, final String aURI) {
- final GeckoInterface geckoInterface = getGeckoInterface();
- if (geckoInterface == null) {
- return;
- }
- geckoInterface.createShortcut(aTitle, aURI);
- }
-
- @JNITarget
- static public int getPreferredIconSize() {
- if (Versions.feature11Plus) {
- ActivityManager am = (ActivityManager)
- getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
- return am.getLauncherLargeIconSize();
- } else {
- switch (getDpi()) {
- case DisplayMetrics.DENSITY_MEDIUM:
- return 48;
- case DisplayMetrics.DENSITY_XHIGH:
- return 96;
- case DisplayMetrics.DENSITY_HIGH:
- default:
- return 72;
- }
- }
- }
-
- @WrapForJNI(stubName = "GetHandlersForMimeTypeWrapper")
- static String[] getHandlersForMimeType(String aMimeType, String aAction) {
- final GeckoInterface geckoInterface = getGeckoInterface();
- if (geckoInterface == null) {
- return new String[] {};
- }
- return geckoInterface.getHandlersForMimeType(aMimeType, aAction);
- }
-
- @WrapForJNI(stubName = "GetHandlersForURLWrapper")
- static String[] getHandlersForURL(String aURL, String aAction) {
- final GeckoInterface geckoInterface = getGeckoInterface();
- if (geckoInterface == null) {
- return new String[] {};
- }
- return geckoInterface.getHandlersForURL(aURL, aAction);
- }
-
- @WrapForJNI(stubName = "GetHWEncoderCapability")
- static boolean getHWEncoderCapability() {
- return HardwareCodecCapabilityUtils.getHWEncoderCapability();
- }
-
- @WrapForJNI(stubName = "GetHWDecoderCapability")
- static boolean getHWDecoderCapability() {
- return HardwareCodecCapabilityUtils.getHWDecoderCapability();
- }
-
- static List<ResolveInfo> queryIntentActivities(Intent intent) {
- final PackageManager pm = getApplicationContext().getPackageManager();
-
- // Exclude any non-exported activities: we can't open them even if we want to!
- // Bug 1031569 has some details.
- final ArrayList<ResolveInfo> list = new ArrayList<>();
- for (ResolveInfo ri: pm.queryIntentActivities(intent, 0)) {
- if (ri.activityInfo.exported) {
- list.add(ri);
- }
- }
-
- return list;
- }
-
- @WrapForJNI(stubName = "GetExtensionFromMimeTypeWrapper")
- static String getExtensionFromMimeType(String aMimeType) {
- return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
- }
-
- @WrapForJNI(stubName = "GetMimeTypeFromExtensionsWrapper")
- static String getMimeTypeFromExtensions(String aFileExt) {
- StringTokenizer st = new StringTokenizer(aFileExt, ".,; ");
- String type = null;
- String subType = null;
- while (st.hasMoreElements()) {
- String ext = st.nextToken();
- String mt = getMimeTypeFromExtension(ext);
- if (mt == null)
- continue;
- int slash = mt.indexOf('/');
- String tmpType = mt.substring(0, slash);
- if (!tmpType.equalsIgnoreCase(type))
- type = type == null ? tmpType : "*";
- String tmpSubType = mt.substring(slash + 1);
- if (!tmpSubType.equalsIgnoreCase(subType))
- subType = subType == null ? tmpSubType : "*";
- }
- if (type == null)
- type = "*";
- if (subType == null)
- subType = "*";
- return type + "/" + subType;
- }
-
- static boolean isUriSafeForScheme(Uri aUri) {
- // Bug 794034 - We don't want to pass MWI or USSD codes to the
- // dialer, and ensure the Uri class doesn't parse a URI
- // containing a fragment ('#')
- final String scheme = aUri.getScheme();
- if ("tel".equals(scheme) || "sms".equals(scheme)) {
- final String number = aUri.getSchemeSpecificPart();
- if (number.contains("#") || number.contains("*") || aUri.getFragment() != null) {
- return false;
- }
- }
- return true;
- }
-
- @WrapForJNI
- public static boolean openUriExternal(String targetURI,
- String mimeType,
- String packageName,
- String className,
- String action,
- String title) {
- final GeckoInterface geckoInterface = getGeckoInterface();
- if (geckoInterface == null) {
- return false;
- }
- return geckoInterface.openUriExternal(targetURI, mimeType, packageName, className, action, title);
- }
-
- /**
- * Only called from GeckoApp.
- */
- public static void setNotificationClient(NotificationClient client) {
- if (notificationClient == null) {
- notificationClient = client;
- } else {
- Log.d(LOGTAG, "Notification client already set");
- }
- }
-
- @WrapForJNI(stubName = "ShowPersistentAlertNotificationWrapper")
- public static void showPersistentAlertNotification(
- String aPersistentData,
- String aImageUrl, String aAlertTitle, String aAlertText,
- String aAlertCookie, String aAlertName, String aHost) {
- Intent notificationIntent = GeckoService.getIntentToCreateServices(
- getApplicationContext(), "persistent-notification-click", aPersistentData);
- int notificationID = aAlertName.hashCode();
- PendingIntent contentIntent = PendingIntent.getService(
- getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
- }
-
- @WrapForJNI(stubName = "ShowAlertNotificationWrapper")
- public static void showAlertNotification(String aImageUrl, String aAlertTitle, String aAlertText, String aAlertCookie, String aAlertName, String aHost) {
- // The intent to launch when the user clicks the expanded notification
- Intent notificationIntent = new Intent(ACTION_ALERT_CALLBACK);
- notificationIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- int notificationID = aAlertName.hashCode();
-
- // Put the strings into the intent as an URI "alert:?name=<alertName>&app=<appName>&cookie=<cookie>"
- Uri.Builder b = new Uri.Builder();
- Uri dataUri = b.scheme("alert").path(Integer.toString(notificationID))
- .appendQueryParameter("name", aAlertName)
- .appendQueryParameter("cookie", aAlertCookie)
- .build();
- notificationIntent.setData(dataUri);
- PendingIntent contentIntent = PendingIntent.getActivity(
- getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- ALERT_COOKIES.put(aAlertName, aAlertCookie);
- callObserver(aAlertName, "alertshow", aAlertCookie);
-
- notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
- }
-
- @WrapForJNI
- public static void alertsProgressListener_OnProgress(String aAlertName, long aProgress, long aProgressMax, String aAlertText) {
- int notificationID = aAlertName.hashCode();
- notificationClient.update(notificationID, aProgress, aProgressMax, aAlertText);
- }
-
- @WrapForJNI
- public static void closeNotification(String aAlertName) {
- String alertCookie = ALERT_COOKIES.get(aAlertName);
- if (alertCookie != null) {
- callObserver(aAlertName, "alertfinished", alertCookie);
- ALERT_COOKIES.remove(aAlertName);
- }
-
- removeObserver(aAlertName);
-
- int notificationID = aAlertName.hashCode();
- notificationClient.remove(notificationID);
- }
-
- public static void handleNotification(String aAction, String aAlertName, String aAlertCookie) {
- int notificationID = aAlertName.hashCode();
-
- if (ACTION_ALERT_CALLBACK.equals(aAction)) {
- callObserver(aAlertName, "alertclickcallback", aAlertCookie);
-
- if (notificationClient.isOngoing(notificationID)) {
- // When clicked, keep the notification if it displays progress
- return;
- }
- }
- closeNotification(aAlertName);
- }
-
- @WrapForJNI(stubName = "GetDpiWrapper")
- public static int getDpi() {
- if (sDensityDpi == 0) {
- sDensityDpi = getApplicationContext().getResources().getDisplayMetrics().densityDpi;
- }
-
- return sDensityDpi;
- }
-
- @WrapForJNI
- public static float getDensity() {
- return getApplicationContext().getResources().getDisplayMetrics().density;
- }
-
- private static boolean isHighMemoryDevice() {
- return HardwareUtils.getMemSize() > HIGH_MEMORY_DEVICE_THRESHOLD_MB;
- }
-
- /**
- * Returns the colour depth of the default screen. This will either be
- * 24 or 16.
- */
- @WrapForJNI(stubName = "GetScreenDepthWrapper")
- public static synchronized int getScreenDepth() {
- if (sScreenDepth == 0) {
- sScreenDepth = 16;
- PixelFormat info = new PixelFormat();
- final WindowManager wm = (WindowManager)
- getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
- PixelFormat.getPixelFormatInfo(wm.getDefaultDisplay().getPixelFormat(), info);
- if (info.bitsPerPixel >= 24 && isHighMemoryDevice()) {
- sScreenDepth = 24;
- }
- }
-
- return sScreenDepth;
- }
-
- @WrapForJNI
- public static synchronized void setScreenDepthOverride(int aScreenDepth) {
- if (sScreenDepth != 0) {
- Log.e(LOGTAG, "Tried to override screen depth after it's already been set");
- return;
- }
-
- sScreenDepth = aScreenDepth;
- }
-
- @WrapForJNI
- public static void setFullScreen(boolean fullscreen) {
- if (getGeckoInterface() != null)
- getGeckoInterface().setFullScreen(fullscreen);
- }
-
- @WrapForJNI
- public static void performHapticFeedback(boolean aIsLongPress) {
- // Don't perform haptic feedback if a vibration is currently playing,
- // because the haptic feedback will nuke the vibration.
- if (!sVibrationMaybePlaying || System.nanoTime() >= sVibrationEndTime) {
- LayerView layerView = getLayerView();
- layerView.performHapticFeedback(aIsLongPress ?
- HapticFeedbackConstants.LONG_PRESS :
- HapticFeedbackConstants.VIRTUAL_KEY);
- }
- }
-
- private static Vibrator vibrator() {
- return (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE);
- }
-
- // Helper method to convert integer array to long array.
- private static long[] convertIntToLongArray(int[] input) {
- long[] output = new long[input.length];
- for (int i = 0; i < input.length; i++) {
- output[i] = input[i];
- }
- return output;
- }
-
- // Vibrate only if haptic feedback is enabled.
- public static void vibrateOnHapticFeedbackEnabled(int[] milliseconds) {
- if (Settings.System.getInt(getApplicationContext().getContentResolver(),
- Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) > 0) {
- vibrate(convertIntToLongArray(milliseconds), -1);
- }
- }
-
- @WrapForJNI(stubName = "Vibrate1")
- public static void vibrate(long milliseconds) {
- sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
- sVibrationMaybePlaying = true;
- vibrator().vibrate(milliseconds);
- }
-
- @WrapForJNI(stubName = "VibrateA")
- public static void vibrate(long[] pattern, int repeat) {
- // If pattern.length is even, the last element in the pattern is a
- // meaningless delay, so don't include it in vibrationDuration.
- long vibrationDuration = 0;
- int iterLen = pattern.length - (pattern.length % 2 == 0 ? 1 : 0);
- for (int i = 0; i < iterLen; i++) {
- vibrationDuration += pattern[i];
- }
-
- sVibrationEndTime = System.nanoTime() + vibrationDuration * 1000000;
- sVibrationMaybePlaying = true;
- vibrator().vibrate(pattern, repeat);
- }
-
- @WrapForJNI
- public static void cancelVibrate() {
- sVibrationMaybePlaying = false;
- sVibrationEndTime = 0;
- vibrator().cancel();
- }
-
- @WrapForJNI
- public static void setKeepScreenOn(final boolean on) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // TODO
- }
- });
- }
-
- @WrapForJNI
- public static void notifyDefaultPrevented(final boolean defaultPrevented) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- LayerView view = getLayerView();
- PanZoomController controller = (view == null ? null : view.getPanZoomController());
- if (controller != null) {
- controller.notifyDefaultActionPrevented(defaultPrevented);
- }
- }
- });
- }
-
- @WrapForJNI
- public static boolean isNetworkLinkUp() {
- ConnectivityManager cm = (ConnectivityManager)
- getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- try {
- NetworkInfo info = cm.getActiveNetworkInfo();
- if (info == null || !info.isConnected())
- return false;
- } catch (SecurityException se) {
- return false;
- }
- return true;
- }
-
- @WrapForJNI
- public static boolean isNetworkLinkKnown() {
- ConnectivityManager cm = (ConnectivityManager)
- getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- try {
- if (cm.getActiveNetworkInfo() == null)
- return false;
- } catch (SecurityException se) {
- return false;
- }
- return true;
- }
-
- @WrapForJNI
- public static int networkLinkType() {
- ConnectivityManager cm = (ConnectivityManager)
- getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo info = cm.getActiveNetworkInfo();
- if (info == null) {
- return LINK_TYPE_UNKNOWN;
- }
-
- switch (info.getType()) {
- case ConnectivityManager.TYPE_ETHERNET:
- return LINK_TYPE_ETHERNET;
- case ConnectivityManager.TYPE_WIFI:
- return LINK_TYPE_WIFI;
- case ConnectivityManager.TYPE_WIMAX:
- return LINK_TYPE_WIMAX;
- case ConnectivityManager.TYPE_MOBILE:
- break; // We will handle sub-types after the switch.
- default:
- Log.w(LOGTAG, "Ignoring the current network type.");
- return LINK_TYPE_UNKNOWN;
- }
-
- TelephonyManager tm = (TelephonyManager)
- getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
- if (tm == null) {
- Log.e(LOGTAG, "Telephony service does not exist");
- return LINK_TYPE_UNKNOWN;
- }
-
- switch (tm.getNetworkType()) {
- case TelephonyManager.NETWORK_TYPE_IDEN:
- case TelephonyManager.NETWORK_TYPE_CDMA:
- case TelephonyManager.NETWORK_TYPE_GPRS:
- return LINK_TYPE_2G;
- case TelephonyManager.NETWORK_TYPE_1xRTT:
- case TelephonyManager.NETWORK_TYPE_EDGE:
- return LINK_TYPE_2G; // 2.5G
- case TelephonyManager.NETWORK_TYPE_UMTS:
- case TelephonyManager.NETWORK_TYPE_EVDO_0:
- return LINK_TYPE_3G;
- case TelephonyManager.NETWORK_TYPE_HSPA:
- case TelephonyManager.NETWORK_TYPE_HSDPA:
- case TelephonyManager.NETWORK_TYPE_HSUPA:
- case TelephonyManager.NETWORK_TYPE_EVDO_A:
- case TelephonyManager.NETWORK_TYPE_EVDO_B:
- case TelephonyManager.NETWORK_TYPE_EHRPD:
- return LINK_TYPE_3G; // 3.5G
- case TelephonyManager.NETWORK_TYPE_HSPAP:
- return LINK_TYPE_3G; // 3.75G
- case TelephonyManager.NETWORK_TYPE_LTE:
- return LINK_TYPE_4G; // 3.9G
- case TelephonyManager.NETWORK_TYPE_UNKNOWN:
- default:
- Log.w(LOGTAG, "Connected to an unknown mobile network!");
- return LINK_TYPE_UNKNOWN;
- }
- }
-
- @WrapForJNI(stubName = "GetSystemColoursWrapper")
- public static int[] getSystemColors() {
- // attrsAppearance[] must correspond to AndroidSystemColors structure in android/AndroidBridge.h
- final int[] attrsAppearance = {
- android.R.attr.textColor,
- android.R.attr.textColorPrimary,
- android.R.attr.textColorPrimaryInverse,
- android.R.attr.textColorSecondary,
- android.R.attr.textColorSecondaryInverse,
- android.R.attr.textColorTertiary,
- android.R.attr.textColorTertiaryInverse,
- android.R.attr.textColorHighlight,
- android.R.attr.colorForeground,
- android.R.attr.colorBackground,
- android.R.attr.panelColorForeground,
- android.R.attr.panelColorBackground
- };
-
- int[] result = new int[attrsAppearance.length];
-
- final ContextThemeWrapper contextThemeWrapper =
- new ContextThemeWrapper(getApplicationContext(), android.R.style.TextAppearance);
-
- final TypedArray appearance = contextThemeWrapper.getTheme().obtainStyledAttributes(attrsAppearance);
-
- if (appearance != null) {
- for (int i = 0; i < appearance.getIndexCount(); i++) {
- int idx = appearance.getIndex(i);
- int color = appearance.getColor(idx, 0);
- result[idx] = color;
- }
- appearance.recycle();
- }
-
- return result;
- }
-
- @WrapForJNI
- public static void killAnyZombies() {
- GeckoProcessesVisitor visitor = new GeckoProcessesVisitor() {
- @Override
- public boolean callback(int pid) {
- if (pid != android.os.Process.myPid())
- android.os.Process.killProcess(pid);
- return true;
- }
- };
-
- EnumerateGeckoProcesses(visitor);
- }
-
- interface GeckoProcessesVisitor {
- boolean callback(int pid);
- }
-
- private static void EnumerateGeckoProcesses(GeckoProcessesVisitor visiter) {
- int pidColumn = -1;
- int userColumn = -1;
-
- try {
- // run ps and parse its output
- java.lang.Process ps = Runtime.getRuntime().exec("ps");
- BufferedReader in = new BufferedReader(new InputStreamReader(ps.getInputStream()),
- 2048);
-
- String headerOutput = in.readLine();
-
- // figure out the column offsets. We only care about the pid and user fields
- StringTokenizer st = new StringTokenizer(headerOutput);
-
- int tokenSoFar = 0;
- while (st.hasMoreTokens()) {
- String next = st.nextToken();
- if (next.equalsIgnoreCase("PID"))
- pidColumn = tokenSoFar;
- else if (next.equalsIgnoreCase("USER"))
- userColumn = tokenSoFar;
- tokenSoFar++;
- }
-
- // alright, the rest are process entries.
- String psOutput = null;
- while ((psOutput = in.readLine()) != null) {
- String[] split = psOutput.split("\\s+");
- if (split.length <= pidColumn || split.length <= userColumn)
- continue;
- int uid = android.os.Process.getUidForName(split[userColumn]);
- if (uid == android.os.Process.myUid() &&
- !split[split.length - 1].equalsIgnoreCase("ps")) {
- int pid = Integer.parseInt(split[pidColumn]);
- boolean keepGoing = visiter.callback(pid);
- if (keepGoing == false)
- break;
- }
- }
- in.close();
- }
- catch (Exception e) {
- Log.w(LOGTAG, "Failed to enumerate Gecko processes.", e);
- }
- }
-
- public static String getAppNameByPID(int pid) {
- BufferedReader cmdlineReader = null;
- String path = "/proc/" + pid + "/cmdline";
- try {
- File cmdlineFile = new File(path);
- if (!cmdlineFile.exists())
- return "";
- cmdlineReader = new BufferedReader(new FileReader(cmdlineFile));
- return cmdlineReader.readLine().trim();
- } catch (Exception ex) {
- return "";
- } finally {
- if (null != cmdlineReader) {
- try {
- cmdlineReader.close();
- } catch (Exception e) { }
- }
- }
- }
-
- public static void listOfOpenFiles() {
- int pidColumn = -1;
- int nameColumn = -1;
-
- try {
- String filter = GeckoProfile.get(getApplicationContext()).getDir().toString();
- Log.d(LOGTAG, "[OPENFILE] Filter: " + filter);
-
- // run lsof and parse its output
- java.lang.Process lsof = Runtime.getRuntime().exec("lsof");
- BufferedReader in = new BufferedReader(new InputStreamReader(lsof.getInputStream()), 2048);
-
- String headerOutput = in.readLine();
- StringTokenizer st = new StringTokenizer(headerOutput);
- int token = 0;
- while (st.hasMoreTokens()) {
- String next = st.nextToken();
- if (next.equalsIgnoreCase("PID"))
- pidColumn = token;
- else if (next.equalsIgnoreCase("NAME"))
- nameColumn = token;
- token++;
- }
-
- // alright, the rest are open file entries.
- Map<Integer, String> pidNameMap = new TreeMap<Integer, String>();
- String output = null;
- while ((output = in.readLine()) != null) {
- String[] split = output.split("\\s+");
- if (split.length <= pidColumn || split.length <= nameColumn)
- continue;
- final Integer pid = Integer.valueOf(split[pidColumn]);
- String name = pidNameMap.get(pid);
- if (name == null) {
- name = getAppNameByPID(pid.intValue());
- pidNameMap.put(pid, name);
- }
- String file = split[nameColumn];
- if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(file) && file.startsWith(filter))
- Log.d(LOGTAG, "[OPENFILE] " + name + "(" + split[pidColumn] + ") : " + file);
- }
- in.close();
- } catch (Exception e) { }
- }
-
- @WrapForJNI(stubName = "GetIconForExtensionWrapper")
- public static byte[] getIconForExtension(String aExt, int iconSize) {
- try {
- if (iconSize <= 0)
- iconSize = 16;
-
- if (aExt != null && aExt.length() > 1 && aExt.charAt(0) == '.')
- aExt = aExt.substring(1);
-
- PackageManager pm = getApplicationContext().getPackageManager();
- Drawable icon = getDrawableForExtension(pm, aExt);
- if (icon == null) {
- // Use a generic icon
- icon = pm.getDefaultActivityIcon();
- }
-
- Bitmap bitmap = ((BitmapDrawable)icon).getBitmap();
- if (bitmap.getWidth() != iconSize || bitmap.getHeight() != iconSize)
- bitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, true);
-
- ByteBuffer buf = ByteBuffer.allocate(iconSize * iconSize * 4);
- bitmap.copyPixelsToBuffer(buf);
-
- return buf.array();
- }
- catch (Exception e) {
- Log.w(LOGTAG, "getIconForExtension failed.", e);
- return null;
- }
- }
-
- public static String getMimeTypeFromExtension(String ext) {
- final MimeTypeMap mtm = MimeTypeMap.getSingleton();
- return mtm.getMimeTypeFromExtension(ext);
- }
-
- private static Drawable getDrawableForExtension(PackageManager pm, String aExt) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- final String mimeType = getMimeTypeFromExtension(aExt);
- if (mimeType != null && mimeType.length() > 0)
- intent.setType(mimeType);
- else
- return null;
-
- List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
- if (list.size() == 0)
- return null;
-
- ResolveInfo resolveInfo = list.get(0);
-
- if (resolveInfo == null)
- return null;
-
- ActivityInfo activityInfo = resolveInfo.activityInfo;
-
- return activityInfo.loadIcon(pm);
- }
-
- @WrapForJNI
- public static boolean getShowPasswordSetting() {
- try {
- int showPassword =
- Settings.System.getInt(getApplicationContext().getContentResolver(),
- Settings.System.TEXT_SHOW_PASSWORD, 1);
- return (showPassword > 0);
- }
- catch (Exception e) {
- return true;
- }
- }
-
- @WrapForJNI(stubName = "AddPluginViewWrapper")
- public static void addPluginView(View view,
- float x, float y,
- float w, float h,
- boolean isFullScreen) {
- if (getGeckoInterface() != null)
- getGeckoInterface().addPluginView(view, new RectF(x, y, x + w, y + h), isFullScreen);
- }
-
- @WrapForJNI
- public static void removePluginView(View view, boolean isFullScreen) {
- if (getGeckoInterface() != null)
- getGeckoInterface().removePluginView(view, isFullScreen);
- }
-
- /**
- * A plugin that wish to be loaded in the WebView must provide this permission
- * in their AndroidManifest.xml.
- */
- public static final String PLUGIN_ACTION = "android.webkit.PLUGIN";
- public static final String PLUGIN_PERMISSION = "android.webkit.permission.PLUGIN";
-
- private static final String PLUGIN_SYSTEM_LIB = "/system/lib/plugins/";
-
- private static final String PLUGIN_TYPE = "type";
- private static final String TYPE_NATIVE = "native";
- public static final ArrayList<PackageInfo> mPackageInfoCache = new ArrayList<>();
-
- // Returns null if plugins are blocked on the device.
- static String[] getPluginDirectories() {
-
- // Block on Pixel C.
- if ((new File("/system/lib/hw/power.dragon.so")).exists()) {
- Log.w(LOGTAG, "Blocking plugins because of Pixel C device (bug 1255122)");
- return null;
- }
- // An awful hack to detect Tegra devices. Easiest way to do it without spinning up a EGL context.
- boolean isTegra = (new File("/system/lib/hw/gralloc.tegra.so")).exists() ||
- (new File("/system/lib/hw/gralloc.tegra3.so")).exists() ||
- (new File("/sys/class/nvidia-gpu")).exists();
- if (isTegra) {
- // disable on KitKat (bug 957694)
- if (Versions.feature19Plus) {
- Log.w(LOGTAG, "Blocking plugins because of Tegra (bug 957694)");
- return null;
- }
-
- // disable Flash on Tegra ICS with CM9 and other custom firmware (bug 736421)
- final File vfile = new File("/proc/version");
- try {
- if (vfile.canRead()) {
- final BufferedReader reader = new BufferedReader(new FileReader(vfile));
- try {
- final String version = reader.readLine();
- if (version.indexOf("CM9") != -1 ||
- version.indexOf("cyanogen") != -1 ||
- version.indexOf("Nova") != -1) {
- Log.w(LOGTAG, "Blocking plugins because of Tegra 2 + unofficial ICS bug (bug 736421)");
- return null;
- }
- } finally {
- reader.close();
- }
- }
- } catch (IOException ex) {
- // Do nothing.
- }
- }
-
- ArrayList<String> directories = new ArrayList<String>();
- PackageManager pm = getApplicationContext().getPackageManager();
- List<ResolveInfo> plugins = pm.queryIntentServices(new Intent(PLUGIN_ACTION),
- PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
-
- synchronized (mPackageInfoCache) {
-
- // clear the list of existing packageInfo objects
- mPackageInfoCache.clear();
-
-
- for (ResolveInfo info : plugins) {
-
- // retrieve the plugin's service information
- ServiceInfo serviceInfo = info.serviceInfo;
- if (serviceInfo == null) {
- Log.w(LOGTAG, "Ignoring bad plugin.");
- continue;
- }
-
- // Blacklist HTC's flash lite.
- // See bug #704516 - We're not quite sure what Flash Lite does,
- // but loading it causes Flash to give errors and fail to draw.
- if (serviceInfo.packageName.equals("com.htc.flashliteplugin")) {
- Log.w(LOGTAG, "Skipping HTC's flash lite plugin");
- continue;
- }
-
-
- // Retrieve information from the plugin's manifest.
- PackageInfo pkgInfo;
- try {
- pkgInfo = pm.getPackageInfo(serviceInfo.packageName,
- PackageManager.GET_PERMISSIONS
- | PackageManager.GET_SIGNATURES);
- } catch (Exception e) {
- Log.w(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
- continue;
- }
-
- if (pkgInfo == null) {
- Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Could not load package information.");
- continue;
- }
-
- /*
- * find the location of the plugin's shared library. The default
- * is to assume the app is either a user installed app or an
- * updated system app. In both of these cases the library is
- * stored in the app's data directory.
- */
- String directory = pkgInfo.applicationInfo.dataDir + "/lib";
- final int appFlags = pkgInfo.applicationInfo.flags;
- final int updatedSystemFlags = ApplicationInfo.FLAG_SYSTEM |
- ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
-
- // preloaded system app with no user updates
- if ((appFlags & updatedSystemFlags) == ApplicationInfo.FLAG_SYSTEM) {
- directory = PLUGIN_SYSTEM_LIB + pkgInfo.packageName;
- }
-
- // check if the plugin has the required permissions
- String permissions[] = pkgInfo.requestedPermissions;
- if (permissions == null) {
- Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Does not have required permission.");
- continue;
- }
- boolean permissionOk = false;
- for (String permit : permissions) {
- if (PLUGIN_PERMISSION.equals(permit)) {
- permissionOk = true;
- break;
- }
- }
- if (!permissionOk) {
- Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Does not have required permission (2).");
- continue;
- }
-
- // check to ensure the plugin is properly signed
- Signature signatures[] = pkgInfo.signatures;
- if (signatures == null) {
- Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Not signed.");
- continue;
- }
-
- // determine the type of plugin from the manifest
- if (serviceInfo.metaData == null) {
- Log.e(LOGTAG, "The plugin '" + serviceInfo.name + "' has no defined type.");
- continue;
- }
-
- String pluginType = serviceInfo.metaData.getString(PLUGIN_TYPE);
- if (!TYPE_NATIVE.equals(pluginType)) {
- Log.e(LOGTAG, "Unrecognized plugin type: " + pluginType);
- continue;
- }
-
- try {
- Class<?> cls = getPluginClass(serviceInfo.packageName, serviceInfo.name);
-
- //TODO implement any requirements of the plugin class here!
- boolean classFound = true;
-
- if (!classFound) {
- Log.e(LOGTAG, "The plugin's class' " + serviceInfo.name + "' does not extend the appropriate class.");
- continue;
- }
-
- } catch (NameNotFoundException e) {
- Log.e(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
- continue;
- } catch (ClassNotFoundException e) {
- Log.e(LOGTAG, "Can't find plugin's class: " + serviceInfo.name);
- continue;
- }
-
- // if all checks have passed then make the plugin available
- mPackageInfoCache.add(pkgInfo);
- directories.add(directory);
- }
- }
-
- return directories.toArray(new String[directories.size()]);
- }
-
- static String getPluginPackage(String pluginLib) {
-
- if (pluginLib == null || pluginLib.length() == 0) {
- return null;
- }
-
- synchronized (mPackageInfoCache) {
- for (PackageInfo pkgInfo : mPackageInfoCache) {
- if (pluginLib.contains(pkgInfo.packageName)) {
- return pkgInfo.packageName;
- }
- }
- }
-
- return null;
- }
-
- static Class<?> getPluginClass(String packageName, String className)
- throws NameNotFoundException, ClassNotFoundException {
- Context pluginContext = getApplicationContext().createPackageContext(packageName,
- Context.CONTEXT_INCLUDE_CODE |
- Context.CONTEXT_IGNORE_SECURITY);
- ClassLoader pluginCL = pluginContext.getClassLoader();
- return pluginCL.loadClass(className);
- }
-
- @WrapForJNI(allowMultithread = true)
- public static Class<?> loadPluginClass(String className, String libName) {
- if (getGeckoInterface() == null)
- return null;
- try {
- final String packageName = getPluginPackage(libName);
- final int contextFlags = Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY;
- final Context pluginContext = getApplicationContext().createPackageContext(
- packageName, contextFlags);
- return pluginContext.getClassLoader().loadClass(className);
- } catch (java.lang.ClassNotFoundException cnfe) {
- Log.w(LOGTAG, "Couldn't find plugin class " + className, cnfe);
- return null;
- } catch (android.content.pm.PackageManager.NameNotFoundException nnfe) {
- Log.w(LOGTAG, "Couldn't find package.", nnfe);
- return null;
- }
- }
-
- private static Context sApplicationContext;
- private static ContextGetter sContextGetter;
-
- @Deprecated
- @WrapForJNI(allowMultithread = true)
- public static Context getContext() {
- return sContextGetter.getContext();
- }
-
- public static void setContextGetter(ContextGetter cg) {
- sContextGetter = cg;
- }
-
- @WrapForJNI(allowMultithread = true)
- public static Context getApplicationContext() {
- return sApplicationContext;
- }
-
- public static void setApplicationContext(final Context context) {
- sApplicationContext = context;
- }
-
- public static SharedPreferences getSharedPreferences() {
- if (sContextGetter == null) {
- throw new IllegalStateException("No ContextGetter; cannot fetch prefs.");
- }
- return sContextGetter.getSharedPreferences();
- }
-
- public interface AppStateListener {
- public void onPause();
- public void onResume();
- public void onOrientationChanged();
- }
-
- public interface GeckoInterface {
- public GeckoProfile getProfile();
- public Activity getActivity();
- public String getDefaultUAString();
- public LocationListener getLocationListener();
- public SensorEventListener getSensorEventListener();
- public void doRestart();
- public void setFullScreen(boolean fullscreen);
- public void addPluginView(View view, final RectF rect, final boolean isFullScreen);
- public void removePluginView(final View view, final boolean isFullScreen);
- public void enableCameraView();
- public void disableCameraView();
- public void addAppStateListener(AppStateListener listener);
- public void removeAppStateListener(AppStateListener listener);
- public View getCameraView();
- public void notifyWakeLockChanged(String topic, String state);
- public void onInputMethodChanged(String inputMethod);
- public boolean areTabsShown();
- public AbsoluteLayout getPluginContainer();
- public void notifyCheckUpdateResult(String result);
- public void invalidateOptionsMenu();
-
- /**
- * Create a shortcut -- generally a home-screen icon -- linking the given title to the given URI.
- * <p>
- * This method is always invoked on the Gecko thread.
- *
- * @param title of URI to link to.
- * @param URI to link to.
- */
- public void createShortcut(String title, String URI);
-
- /**
- * Check if the given URI is visited.
- * <p/>
- * If it has been visited, call {@link GeckoAppShell#notifyUriVisited(String)}. (If it
- * has not been visited, do nothing.)
- * <p/>
- * This method is always invoked on the Gecko thread.
- *
- * @param uri to check.
- */
- public void checkUriVisited(String uri);
-
- /**
- * Mark the given URI as visited in Gecko.
- * <p/>
- * Implementors may maintain some local store of visited URIs in order to be able to
- * answer {@link #checkUriVisited(String)} requests affirmatively.
- * <p/>
- * This method is always invoked on the Gecko thread.
- *
- * @param uri to mark.
- */
- public void markUriVisited(final String uri);
-
- /**
- * Set the title of the given URI, as determined by Gecko.
- * <p/>
- * This method is always invoked on the Gecko thread.
- *
- * @param uri given.
- * @param title to associate with the given URI.
- */
- public void setUriTitle(final String uri, final String title);
-
- public void setAccessibilityEnabled(boolean enabled);
-
- public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title);
-
- public String[] getHandlersForMimeType(String mimeType, String action);
- public String[] getHandlersForURL(String url, String action);
-
- /**
- * URI of the underlying chrome window to be opened, or null to use the default GeckoView
- * XUL container <tt>chrome://browser/content/geckoview.xul</tt>. See
- * <a href="https://developer.mozilla.org/en/docs/toolkit.defaultChromeURI">https://developer.mozilla.org/en/docs/toolkit.defaultChromeURI</a>
- *
- * @return URI or null.
- */
- String getDefaultChromeURI();
- };
-
- private static GeckoInterface sGeckoInterface;
-
- public static GeckoInterface getGeckoInterface() {
- return sGeckoInterface;
- }
-
- public static void setGeckoInterface(GeckoInterface aGeckoInterface) {
- sGeckoInterface = aGeckoInterface;
- }
-
- public static android.hardware.Camera sCamera;
-
- static native void cameraCallbackBridge(byte[] data);
-
- static final int kPreferredFPS = 25;
- static byte[] sCameraBuffer;
-
-
- @WrapForJNI(stubName = "InitCameraWrapper")
- static int[] initCamera(String aContentType, int aCamera, int aWidth, int aHeight) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- try {
- if (getGeckoInterface() != null)
- getGeckoInterface().enableCameraView();
- } catch (Exception e) { }
- }
- });
-
- // [0] = 0|1 (failure/success)
- // [1] = width
- // [2] = height
- // [3] = fps
- int[] result = new int[4];
- result[0] = 0;
-
- if (android.hardware.Camera.getNumberOfCameras() == 0) {
- return result;
- }
-
- try {
- sCamera = android.hardware.Camera.open(aCamera);
-
- android.hardware.Camera.Parameters params = sCamera.getParameters();
- params.setPreviewFormat(ImageFormat.NV21);
-
- // use the preview fps closest to 25 fps.
- int fpsDelta = 1000;
- try {
- Iterator<Integer> it = params.getSupportedPreviewFrameRates().iterator();
- while (it.hasNext()) {
- int nFps = it.next();
- if (Math.abs(nFps - kPreferredFPS) < fpsDelta) {
- fpsDelta = Math.abs(nFps - kPreferredFPS);
- params.setPreviewFrameRate(nFps);
- }
- }
- } catch (Exception e) {
- params.setPreviewFrameRate(kPreferredFPS);
- }
-
- // set up the closest preview size available
- Iterator<android.hardware.Camera.Size> sit = params.getSupportedPreviewSizes().iterator();
- int sizeDelta = 10000000;
- int bufferSize = 0;
- while (sit.hasNext()) {
- android.hardware.Camera.Size size = sit.next();
- if (Math.abs(size.width * size.height - aWidth * aHeight) < sizeDelta) {
- sizeDelta = Math.abs(size.width * size.height - aWidth * aHeight);
- params.setPreviewSize(size.width, size.height);
- bufferSize = size.width * size.height;
- }
- }
-
- try {
- if (getGeckoInterface() != null) {
- View cameraView = getGeckoInterface().getCameraView();
- if (cameraView instanceof SurfaceView) {
- sCamera.setPreviewDisplay(((SurfaceView)cameraView).getHolder());
- } else if (cameraView instanceof TextureView) {
- sCamera.setPreviewTexture(((TextureView)cameraView).getSurfaceTexture());
- }
- }
- } catch (IOException | RuntimeException e) {
- Log.w(LOGTAG, "Error setPreviewXXX:", e);
- }
-
- sCamera.setParameters(params);
- sCameraBuffer = new byte[(bufferSize * 12) / 8];
- sCamera.addCallbackBuffer(sCameraBuffer);
- sCamera.setPreviewCallbackWithBuffer(new android.hardware.Camera.PreviewCallback() {
- @Override
- public void onPreviewFrame(byte[] data, android.hardware.Camera camera) {
- cameraCallbackBridge(data);
- if (sCamera != null)
- sCamera.addCallbackBuffer(sCameraBuffer);
- }
- });
- sCamera.startPreview();
- params = sCamera.getParameters();
- result[0] = 1;
- result[1] = params.getPreviewSize().width;
- result[2] = params.getPreviewSize().height;
- result[3] = params.getPreviewFrameRate();
- } catch (RuntimeException e) {
- Log.w(LOGTAG, "initCamera RuntimeException.", e);
- result[0] = result[1] = result[2] = result[3] = 0;
- }
- return result;
- }
-
- @WrapForJNI
- static synchronized void closeCamera() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- try {
- if (getGeckoInterface() != null)
- getGeckoInterface().disableCameraView();
- } catch (Exception e) { }
- }
- });
- if (sCamera != null) {
- sCamera.stopPreview();
- sCamera.release();
- sCamera = null;
- sCameraBuffer = null;
- }
- }
-
- /*
- * Battery API related methods.
- */
- @WrapForJNI
- public static void enableBatteryNotifications() {
- GeckoBatteryManager.enableNotifications();
- }
-
- @WrapForJNI(stubName = "HandleGeckoMessageWrapper")
- public static void handleGeckoMessage(final NativeJSContainer message) {
- EventDispatcher.getInstance().dispatchEvent(message);
- message.disposeNative();
- }
-
- @WrapForJNI
- public static void disableBatteryNotifications() {
- GeckoBatteryManager.disableNotifications();
- }
-
- @WrapForJNI(stubName = "GetCurrentBatteryInformationWrapper")
- public static double[] getCurrentBatteryInformation() {
- return GeckoBatteryManager.getCurrentInformation();
- }
-
- @WrapForJNI(stubName = "CheckURIVisited")
- static void checkUriVisited(String uri) {
- final GeckoInterface geckoInterface = getGeckoInterface();
- if (geckoInterface == null) {
- return;
- }
- geckoInterface.checkUriVisited(uri);
- }
-
- @WrapForJNI(stubName = "MarkURIVisited")
- static void markUriVisited(final String uri) {
- final GeckoInterface geckoInterface = getGeckoInterface();
- if (geckoInterface == null) {
- return;
- }
- geckoInterface.markUriVisited(uri);
- }
-
- @WrapForJNI(stubName = "SetURITitle")
- static void setUriTitle(final String uri, final String title) {
- final GeckoInterface geckoInterface = getGeckoInterface();
- if (geckoInterface == null) {
- return;
- }
- geckoInterface.setUriTitle(uri, title);
- }
-
- @WrapForJNI
- static void hideProgressDialog() {
- // unused stub
- }
-
- /*
- * WebSMS related methods.
- */
- public static void sendMessage(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify) {
- if (!SmsManager.isEnabled()) {
- return;
- }
-
- SmsManager.getInstance().send(aNumber, aMessage, aRequestId, aShouldNotify);
- }
-
- @WrapForJNI(stubName = "SendMessageWrapper")
- public static void sendMessage(String aNumber, String aMessage, int aRequestId) {
- sendMessage(aNumber, aMessage, aRequestId, /* shouldNotify */ true);
- }
-
- @WrapForJNI(stubName = "GetMessageWrapper")
- public static void getMessage(int aMessageId, int aRequestId) {
- if (!SmsManager.isEnabled()) {
- return;
- }
-
- SmsManager.getInstance().getMessage(aMessageId, aRequestId);
- }
-
- @WrapForJNI(stubName = "DeleteMessageWrapper")
- public static void deleteMessage(int aMessageId, int aRequestId) {
- if (!SmsManager.isEnabled()) {
- return;
- }
-
- SmsManager.getInstance().deleteMessage(aMessageId, aRequestId);
- }
-
- @WrapForJNI
- public static void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId) {
- if (!SmsManager.isEnabled()) {
- return;
- }
-
- SmsManager.getInstance().markMessageRead(aMessageId, aValue, aSendReadReport, aRequestId);
- }
-
- @WrapForJNI(stubName = "CreateMessageCursorWrapper")
- public static void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
- if (!SmsManager.isEnabled()) {
- return;
- }
-
- SmsManager.getInstance().createMessageCursor(aStartDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, aThreadId, aReverse, aRequestId);
- }
-
- @WrapForJNI(stubName = "GetNextMessageWrapper")
- public static void getNextMessage(int aRequestId) {
- if (!SmsManager.isEnabled()) {
- return;
- }
-
- SmsManager.getInstance().getNextMessage(aRequestId);
- }
-
- @WrapForJNI(stubName = "CreateThreadCursorWrapper")
- public static void createThreadCursor(int aRequestId) {
- Log.i("GeckoAppShell", "CreateThreadCursorWrapper!");
-
- if (!SmsManager.isEnabled()) {
- return;
- }
-
- SmsManager.getInstance().createThreadCursor(aRequestId);
- }
-
- @WrapForJNI(stubName = "GetNextThreadWrapper")
- public static void getNextThread(int aRequestId) {
- if (!SmsManager.isEnabled()) {
- return;
- }
-
- SmsManager.getInstance().getNextThread(aRequestId);
- }
-
- /* Called by JNI from AndroidBridge, and by reflection from tests/BaseTest.java.in */
- @WrapForJNI
- @RobocopTarget
- public static boolean isTablet() {
- return HardwareUtils.isTablet();
- }
-
- private static boolean sImeWasEnabledOnLastResize = false;
- public static void viewSizeChanged() {
- GeckoView v = (GeckoView) getLayerView();
- if (v == null) {
- return;
- }
- boolean imeIsEnabled = v.isIMEEnabled();
- if (imeIsEnabled && !sImeWasEnabledOnLastResize) {
- // The IME just came up after not being up, so let's scroll
- // to the focused input.
- notifyObservers("ScrollTo:FocusedInput", "");
- }
- sImeWasEnabledOnLastResize = imeIsEnabled;
- }
-
- @WrapForJNI(stubName = "GetCurrentNetworkInformationWrapper")
- public static double[] getCurrentNetworkInformation() {
- return GeckoNetworkManager.getInstance().getCurrentInformation();
- }
-
- @WrapForJNI
- public static void enableNetworkNotifications() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- GeckoNetworkManager.getInstance().enableNotifications();
- }
- });
- }
-
- @WrapForJNI
- public static void disableNetworkNotifications() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- GeckoNetworkManager.getInstance().disableNotifications();
- }
- });
- }
-
- @WrapForJNI(stubName = "GetScreenOrientationWrapper")
- public static short getScreenOrientation() {
- return GeckoScreenOrientation.getInstance().getScreenOrientation().value;
- }
-
- @WrapForJNI
- public static int getScreenAngle() {
- return GeckoScreenOrientation.getInstance().getAngle();
- }
-
- @WrapForJNI
- public static void enableScreenOrientationNotifications() {
- GeckoScreenOrientation.getInstance().enableNotifications();
- }
-
- @WrapForJNI
- public static void disableScreenOrientationNotifications() {
- GeckoScreenOrientation.getInstance().disableNotifications();
- }
-
- @WrapForJNI
- public static void lockScreenOrientation(int aOrientation) {
- GeckoScreenOrientation.getInstance().lock(aOrientation);
- }
-
- @WrapForJNI
- public static void unlockScreenOrientation() {
- GeckoScreenOrientation.getInstance().unlock();
- }
-
- @WrapForJNI
- public static void notifyWakeLockChanged(String topic, String state) {
- if (getGeckoInterface() != null)
- getGeckoInterface().notifyWakeLockChanged(topic, state);
- }
-
- @WrapForJNI(allowMultithread = true)
- public static void registerSurfaceTextureFrameListener(Object surfaceTexture, final int id) {
- ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
- @Override
- public void onFrameAvailable(SurfaceTexture surfaceTexture) {
- GeckoAppShell.onSurfaceTextureFrameAvailable(surfaceTexture, id);
- }
- });
- }
-
- @WrapForJNI(allowMultithread = true)
- public static void unregisterSurfaceTextureFrameListener(Object surfaceTexture) {
- ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(null);
- }
-
- @WrapForJNI
- public static boolean unlockProfile() {
- // Try to kill any zombie Fennec's that might be running
- GeckoAppShell.killAnyZombies();
-
- // Then force unlock this profile
- if (getGeckoInterface() != null) {
- GeckoProfile profile = getGeckoInterface().getProfile();
- File lock = profile.getFile(".parentlock");
- return lock.exists() && lock.delete();
- }
- return false;
- }
-
- @WrapForJNI(stubName = "GetProxyForURIWrapper")
- public static String getProxyForURI(String spec, String scheme, String host, int port) {
- final ProxySelector ps = new ProxySelector();
-
- Proxy proxy = ps.select(scheme, host);
- if (Proxy.NO_PROXY.equals(proxy)) {
- return "DIRECT";
- }
-
- switch (proxy.type()) {
- case HTTP:
- return "PROXY " + proxy.address().toString();
- case SOCKS:
- return "SOCKS " + proxy.address().toString();
- }
-
- return "DIRECT";
- }
-
- @WrapForJNI(allowMultithread = true)
- static InputStream createInputStream(URLConnection connection) throws IOException {
- return connection.getInputStream();
- }
-
- private static class BitmapConnection extends URLConnection {
- private Bitmap bitmap;
-
- BitmapConnection(Bitmap b) throws MalformedURLException, IOException {
- super(null);
- bitmap = b;
- }
-
- @Override
- public void connect() {}
-
- @Override
- public InputStream getInputStream() throws IOException {
- return new BitmapInputStream();
- }
-
- @Override
- public String getContentType() {
- return "image/png";
- }
-
- private final class BitmapInputStream extends PipedInputStream {
- private boolean mHaveConnected = false;
-
- @Override
- public synchronized int read(byte[] buffer, int byteOffset, int byteCount)
- throws IOException {
- if (mHaveConnected) {
- return super.read(buffer, byteOffset, byteCount);
- }
-
- final PipedOutputStream output = new PipedOutputStream();
- connect(output);
- ThreadUtils.postToBackgroundThread(
- new Runnable() {
- @Override
- public void run() {
- try {
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, output);
- output.close();
- } catch (IOException ioe) { }
- }
- });
- mHaveConnected = true;
- return super.read(buffer, byteOffset, byteCount);
- }
- }
- }
-
- @WrapForJNI(allowMultithread = true, narrowChars = true)
- static URLConnection getConnection(String url) {
- try {
- String spec;
- if (url.startsWith("android://")) {
- spec = url.substring(10);
- } else {
- spec = url.substring(8);
- }
-
- // Check if we are loading a package icon.
- try {
- if (spec.startsWith("icon/")) {
- String[] splits = spec.split("/");
- if (splits.length != 2) {
- return null;
- }
- final String pkg = splits[1];
- final PackageManager pm = getApplicationContext().getPackageManager();
- final Drawable d = pm.getApplicationIcon(pkg);
- final Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(d);
- return new BitmapConnection(bitmap);
- }
- } catch (Exception ex) {
- Log.e(LOGTAG, "error", ex);
- }
-
- // if the colon got stripped, put it back
- int colon = spec.indexOf(':');
- if (colon == -1 || colon > spec.indexOf('/')) {
- spec = spec.replaceFirst("/", ":/");
- }
- } catch (Exception ex) {
- return null;
- }
- return null;
- }
-
- @WrapForJNI(allowMultithread = true, narrowChars = true)
- static String connectionGetMimeType(URLConnection connection) {
- return connection.getContentType();
- }
-
- /**
- * Retrieve the absolute path of an external storage directory.
- *
- * @param type The type of directory to return
- * @return Absolute path of the specified directory or null on failure
- */
- @WrapForJNI
- static String getExternalPublicDirectory(final String type) {
- final String state = Environment.getExternalStorageState();
- if (!Environment.MEDIA_MOUNTED.equals(state) &&
- !Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
- // External storage is not available.
- return null;
- }
-
- if ("sdcard".equals(type)) {
- // SD card has a separate path.
- return Environment.getExternalStorageDirectory().getAbsolutePath();
- }
-
- final String systemType;
- if ("downloads".equals(type)) {
- systemType = Environment.DIRECTORY_DOWNLOADS;
- } else if ("pictures".equals(type)) {
- systemType = Environment.DIRECTORY_PICTURES;
- } else if ("videos".equals(type)) {
- systemType = Environment.DIRECTORY_MOVIES;
- } else if ("music".equals(type)) {
- systemType = Environment.DIRECTORY_MUSIC;
- } else if ("apps".equals(type)) {
- File appInternalStorageDirectory = getApplicationContext().getFilesDir();
- return new File(appInternalStorageDirectory, "mozilla").getAbsolutePath();
- } else {
- return null;
- }
- return Environment.getExternalStoragePublicDirectory(systemType).getAbsolutePath();
- }
-
- @WrapForJNI
- static int getMaxTouchPoints() {
- PackageManager pm = getApplicationContext().getPackageManager();
- if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND)) {
- // at least, 5+ fingers.
- return 5;
- } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
- // at least, 2+ fingers.
- return 2;
- } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) {
- // 2 fingers
- return 2;
- } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
- // 1 finger
- return 1;
- }
- return 0;
- }
-
- public static synchronized void resetScreenSize() {
- sScreenSize = null;
- }
-
- @WrapForJNI
- public static synchronized Rect getScreenSize() {
- if (sScreenSize == null) {
- final WindowManager wm = (WindowManager)
- getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
- final Display disp = wm.getDefaultDisplay();
- sScreenSize = new Rect(0, 0, disp.getWidth(), disp.getHeight());
- }
- return sScreenSize;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoBatteryManager.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/* -*- 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.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-import android.os.Build;
-import android.os.SystemClock;
-import android.util.Log;
-
-public class GeckoBatteryManager extends BroadcastReceiver {
- private static final String LOGTAG = "GeckoBatteryManager";
-
- // Those constants should be keep in sync with the ones in:
- // dom/battery/Constants.h
- private final static double kDefaultLevel = 1.0;
- private final static boolean kDefaultCharging = true;
- private final static double kDefaultRemainingTime = 0.0;
- private final static double kUnknownRemainingTime = -1.0;
-
- private static long sLastLevelChange;
- private static boolean sNotificationsEnabled;
- private static double sLevel = kDefaultLevel;
- private static boolean sCharging = kDefaultCharging;
- private static double sRemainingTime = kDefaultRemainingTime;
-
- private static final GeckoBatteryManager sInstance = new GeckoBatteryManager();
-
- private final IntentFilter mFilter;
- private Context mApplicationContext;
- private boolean mIsEnabled;
-
- public static GeckoBatteryManager getInstance() {
- return sInstance;
- }
-
- private GeckoBatteryManager() {
- mFilter = new IntentFilter();
- mFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
- }
-
- public synchronized void start(final Context context) {
- if (mIsEnabled) {
- Log.w(LOGTAG, "Already started!");
- return;
- }
-
- mApplicationContext = context.getApplicationContext();
- // registerReceiver will return null if registering fails.
- if (mApplicationContext.registerReceiver(this, mFilter) == null) {
- Log.e(LOGTAG, "Registering receiver failed");
- } else {
- mIsEnabled = true;
- }
- }
-
- public synchronized void stop() {
- if (!mIsEnabled) {
- Log.w(LOGTAG, "Already stopped!");
- return;
- }
-
- mApplicationContext.unregisterReceiver(this);
- mApplicationContext = null;
- mIsEnabled = false;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
- Log.e(LOGTAG, "Got an unexpected intent!");
- return;
- }
-
- boolean previousCharging = isCharging();
- double previousLevel = getLevel();
-
- // NOTE: it might not be common (in 2012) but technically, Android can run
- // on a device that has no battery so we want to make sure it's not the case
- // before bothering checking for battery state.
- // However, the Galaxy Nexus phone advertises itself as battery-less which
- // force us to special-case the logic.
- // See the Google bug: https://code.google.com/p/android/issues/detail?id=22035
- if (intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false) ||
- Build.MODEL.equals("Galaxy Nexus")) {
- int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
- if (plugged == -1) {
- sCharging = kDefaultCharging;
- Log.e(LOGTAG, "Failed to get the plugged status!");
- } else {
- // Likely, if plugged > 0, it's likely plugged and charging but the doc
- // isn't clear about that.
- sCharging = plugged != 0;
- }
-
- if (sCharging != previousCharging) {
- sRemainingTime = kUnknownRemainingTime;
- // The new remaining time is going to take some time to show up but
- // it's the best way to show a not too wrong value.
- sLastLevelChange = 0;
- }
-
- // We need two doubles because sLevel is a double.
- double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
- double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
- if (current == -1 || max == -1) {
- Log.e(LOGTAG, "Failed to get battery level!");
- sLevel = kDefaultLevel;
- } else {
- sLevel = current / max;
- }
-
- if (sLevel == 1.0 && sCharging) {
- sRemainingTime = kDefaultRemainingTime;
- } else if (sLevel != previousLevel) {
- // Estimate remaining time.
- if (sLastLevelChange != 0) {
- // Use elapsedRealtime() because we want to track time across device sleeps.
- long currentTime = SystemClock.elapsedRealtime();
- long dt = (currentTime - sLastLevelChange) / 1000;
- double dLevel = sLevel - previousLevel;
-
- if (sCharging) {
- if (dLevel < 0) {
- sRemainingTime = kUnknownRemainingTime;
- } else {
- sRemainingTime = Math.round(dt / dLevel * (1.0 - sLevel));
- }
- } else {
- if (dLevel > 0) {
- Log.w(LOGTAG, "When discharging, level should decrease!");
- sRemainingTime = kUnknownRemainingTime;
- } else {
- sRemainingTime = Math.round(dt / -dLevel * sLevel);
- }
- }
-
- sLastLevelChange = currentTime;
- } else {
- // That's the first time we got an update, we can't do anything.
- sLastLevelChange = SystemClock.elapsedRealtime();
- }
- }
- } else {
- sLevel = kDefaultLevel;
- sCharging = kDefaultCharging;
- sRemainingTime = kDefaultRemainingTime;
- }
-
- /*
- * We want to inform listeners if the following conditions are fulfilled:
- * - we have at least one observer;
- * - the charging state or the level has changed.
- *
- * Note: no need to check for a remaining time change given that it's only
- * updated if there is a level change or a charging change.
- *
- * The idea is to prevent doing all the way to the DOM code in the child
- * process to finally not send an event.
- */
- if (sNotificationsEnabled &&
- (previousCharging != isCharging() || previousLevel != getLevel())) {
- GeckoAppShell.notifyBatteryChange(getLevel(), isCharging(), getRemainingTime());
- }
- }
-
- public static boolean isCharging() {
- return sCharging;
- }
-
- public static double getLevel() {
- return sLevel;
- }
-
- public static double getRemainingTime() {
- return sRemainingTime;
- }
-
- public static void enableNotifications() {
- sNotificationsEnabled = true;
- }
-
- public static void disableNotifications() {
- sNotificationsEnabled = false;
- }
-
- public static double[] getCurrentInformation() {
- return new double[] { getLevel(), isCharging() ? 1.0 : 0.0, getRemainingTime() };
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEditable.java
+++ /dev/null
@@ -1,1439 +0,0 @@
-/* -*- 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.lang.reflect.Array;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Semaphore;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.mozglue.JNIObject;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.text.Editable;
-import android.text.InputFilter;
-import android.text.NoCopySpan;
-import android.text.Selection;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.text.style.CharacterStyle;
-import android.util.Log;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
-
-/*
- GeckoEditable implements only some functions of Editable
- The field mText contains the actual underlying
- SpannableStringBuilder/Editable that contains our text.
-*/
-final class GeckoEditable extends JNIObject
- implements InvocationHandler, Editable,
- GeckoEditableClient, GeckoEditableListener, GeckoEventListener {
-
- private static final boolean DEBUG = false;
- private static final String LOGTAG = "GeckoEditable";
-
- // Filters to implement Editable's filtering functionality
- private InputFilter[] mFilters;
-
- private final SpannableStringBuilder mText;
- private final Editable mProxy;
- private final ActionQueue mActionQueue;
-
- // mIcRunHandler is the Handler that currently runs Gecko-to-IC Runnables
- // mIcPostHandler is the Handler to post Gecko-to-IC Runnables to
- // The two can be different when switching from one handler to another
- private Handler mIcRunHandler;
- private Handler mIcPostHandler;
-
- /* package */ GeckoEditableListener mListener;
- /* package */ GeckoView mView;
- /* package */ boolean mInBatchMode; // Used by IC thread
- /* package */ boolean mNeedCompositionUpdate; // Used by IC thread
- private boolean mFocused; // Used by IC thread
- private boolean mGeckoFocused; // Used by Gecko thread
- private boolean mIgnoreSelectionChange; // Used by Gecko thread
- private volatile boolean mSuppressCompositions;
- private volatile boolean mSuppressKeyUp;
-
- private static final int IME_RANGE_CARETPOSITION = 1;
- private static final int IME_RANGE_RAWINPUT = 2;
- private static final int IME_RANGE_SELECTEDRAWTEXT = 3;
- private static final int IME_RANGE_CONVERTEDTEXT = 4;
- private static final int IME_RANGE_SELECTEDCONVERTEDTEXT = 5;
-
- private static final int IME_RANGE_LINE_NONE = 0;
- private static final int IME_RANGE_LINE_DOTTED = 1;
- private static final int IME_RANGE_LINE_DASHED = 2;
- private static final int IME_RANGE_LINE_SOLID = 3;
- private static final int IME_RANGE_LINE_DOUBLE = 4;
- private static final int IME_RANGE_LINE_WAVY = 5;
-
- private static final int IME_RANGE_UNDERLINE = 1;
- private static final int IME_RANGE_FORECOLOR = 2;
- private static final int IME_RANGE_BACKCOLOR = 4;
- private static final int IME_RANGE_LINECOLOR = 8;
-
- @WrapForJNI
- private native void onKeyEvent(int action, int keyCode, int scanCode, int metaState,
- long time, int unicodeChar, int baseUnicodeChar,
- int domPrintableKeyValue, int repeatCount, int flags,
- boolean isSynthesizedImeKey, KeyEvent event);
-
- private void onKeyEvent(KeyEvent event, int action, int savedMetaState,
- boolean isSynthesizedImeKey) {
- // Use a separate action argument so we can override the key's original action,
- // e.g. change ACTION_MULTIPLE to ACTION_DOWN. That way we don't have to allocate
- // a new key event just to change its action field.
- //
- // Normally we expect event.getMetaState() to reflect the current meta-state; however,
- // some software-generated key events may not have event.getMetaState() set, e.g. key
- // events from Swype. Therefore, it's necessary to combine the key's meta-states
- // with the meta-states that we keep separately in KeyListener
- final int metaState = event.getMetaState() | savedMetaState;
- final int unmodifiedMetaState = metaState &
- ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK);
- final int unicodeChar = event.getUnicodeChar(metaState);
- final int domPrintableKeyValue =
- unicodeChar >= ' ' ? unicodeChar :
- unmodifiedMetaState != metaState ? event.getUnicodeChar(unmodifiedMetaState) :
- 0;
- onKeyEvent(action, event.getKeyCode(), event.getScanCode(),
- metaState, event.getEventTime(), unicodeChar,
- // e.g. for Ctrl+A, Android returns 0 for unicodeChar,
- // but Gecko expects 'a', so we return that in baseUnicodeChar.
- event.getUnicodeChar(0), domPrintableKeyValue, event.getRepeatCount(),
- event.getFlags(), isSynthesizedImeKey, event);
- }
-
- @WrapForJNI
- private native void onImeSynchronize();
-
- @WrapForJNI
- private native void onImeAcknowledgeFocus();
-
- @WrapForJNI
- private native void onImeReplaceText(int start, int end, String text);
-
- @WrapForJNI
- private native void onImeAddCompositionRange(int start, int end, int rangeType,
- int rangeStyles, int rangeLineStyle,
- boolean rangeBoldLine, int rangeForeColor,
- int rangeBackColor, int rangeLineColor);
-
- @WrapForJNI
- private native void onImeUpdateComposition(int start, int end);
-
- /* An action that alters the Editable
-
- Each action corresponds to a Gecko event. While the Gecko event is being sent to the Gecko
- thread, the action stays on top of mActions queue. After the Gecko event is processed and
- replied, the action is removed from the queue
- */
- private static final class Action {
- // For input events (keypress, etc.); use with onImeSynchronize
- static final int TYPE_EVENT = 0;
- // For Editable.replace() call; use with onImeReplaceText
- static final int TYPE_REPLACE_TEXT = 1;
- // For Editable.setSpan() call; use with onImeSynchronize
- static final int TYPE_SET_SPAN = 2;
- // For Editable.removeSpan() call; use with onImeSynchronize
- static final int TYPE_REMOVE_SPAN = 3;
- // For focus events (in notifyIME); use with onImeAcknowledgeFocus
- static final int TYPE_ACKNOWLEDGE_FOCUS = 4;
- // For switching handler; use with onImeSynchronize
- static final int TYPE_SET_HANDLER = 5;
- // For updating composition; use with onImeUpdateComposition
- static final int TYPE_UPDATE_COMPOSITION = 6;
-
- final int mType;
- boolean mUpdateComposition;
- int mStart;
- int mEnd;
- CharSequence mSequence;
- Object mSpanObject;
- int mSpanFlags;
- Handler mHandler;
-
- Action(int type) {
- mType = type;
- }
-
- static Action newReplaceText(CharSequence text, int start, int end) {
- if (start < 0 || start > end) {
- Log.e(LOGTAG, "invalid replace text offsets: " + start + " to " + end);
- throw new IllegalArgumentException("invalid replace text offsets");
- }
-
- final Action action = new Action(TYPE_REPLACE_TEXT);
- action.mSequence = text;
- action.mStart = start;
- action.mEnd = end;
- return action;
- }
-
- static Action newSetSpan(Object object, int start, int end, int flags, boolean update) {
- if (start < 0 || start > end) {
- Log.e(LOGTAG, "invalid span offsets: " + start + " to " + end);
- throw new IllegalArgumentException("invalid span offsets");
- }
- final Action action = new Action(TYPE_SET_SPAN);
- action.mSpanObject = object;
- action.mStart = start;
- action.mEnd = end;
- action.mSpanFlags = flags;
- action.mUpdateComposition = update;
- return action;
- }
-
- static Action newRemoveSpan(Object object, boolean update) {
- final Action action = new Action(TYPE_REMOVE_SPAN);
- action.mSpanObject = object;
- action.mUpdateComposition = update;
- return action;
- }
-
- static Action newSetHandler(Handler handler) {
- final Action action = new Action(TYPE_SET_HANDLER);
- action.mHandler = handler;
- return action;
- }
-
- static Action newUpdateComposition(int start, int end) {
- final Action action = new Action(TYPE_UPDATE_COMPOSITION);
- action.mStart = start;
- action.mEnd = end;
- return action;
- }
- }
-
- /* Queue of editing actions sent to Gecko thread that
- the Gecko thread has not responded to yet */
- private final class ActionQueue {
- private final ConcurrentLinkedQueue<Action> mActions;
- private final Semaphore mActionsActive;
- private KeyCharacterMap mKeyMap;
-
- ActionQueue() {
- mActions = new ConcurrentLinkedQueue<Action>();
- mActionsActive = new Semaphore(1);
- }
-
- void offer(Action action) {
- if (DEBUG) {
- assertOnIcThread();
- Log.d(LOGTAG, "offer: Action(" +
- getConstantName(Action.class, "TYPE_", action.mType) + ")");
- }
-
- if (mListener == null) {
- // We haven't initialized or we've been destroyed.
- return;
- }
-
- if (mActions.isEmpty()) {
- mActionsActive.acquireUninterruptibly();
- mActions.offer(action);
- } else synchronized (this) {
- // tryAcquire here in case Gecko thread has just released it
- mActionsActive.tryAcquire();
- mActions.offer(action);
- }
-
- switch (action.mType) {
- case Action.TYPE_EVENT:
- case Action.TYPE_SET_SPAN:
- case Action.TYPE_REMOVE_SPAN:
- case Action.TYPE_SET_HANDLER:
- onImeSynchronize();
- break;
-
- case Action.TYPE_REPLACE_TEXT:
- // Because we get composition styling here essentially for free,
- // we don't need to check if we're in batch mode.
- if (icMaybeSendComposition(
- action.mSequence, /* useEntireText */ true, /* notifyGecko */ false)) {
- mNeedCompositionUpdate = false;
- } else {
- // Since we don't have a composition, we can try sending key events.
- sendCharKeyEvents(action);
- }
- onImeReplaceText(action.mStart, action.mEnd, action.mSequence.toString());
- break;
-
- case Action.TYPE_ACKNOWLEDGE_FOCUS:
- onImeAcknowledgeFocus();
- break;
-
- case Action.TYPE_UPDATE_COMPOSITION:
- onImeUpdateComposition(action.mStart, action.mEnd);
- break;
-
- default:
- throw new IllegalStateException("Action not processed");
- }
- }
-
- private KeyEvent [] synthesizeKeyEvents(CharSequence cs) {
- try {
- if (mKeyMap == null) {
- mKeyMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
- }
- } catch (Exception e) {
- // KeyCharacterMap.UnavailableException is not found on Gingerbread;
- // besides, it seems like HC and ICS will throw something other than
- // KeyCharacterMap.UnavailableException; so use a generic Exception here
- return null;
- }
- KeyEvent [] keyEvents = mKeyMap.getEvents(cs.toString().toCharArray());
- if (keyEvents == null || keyEvents.length == 0) {
- return null;
- }
- return keyEvents;
- }
-
- private void sendCharKeyEvents(Action action) {
- if (action.mSequence.length() != 1 ||
- (action.mSequence instanceof Spannable &&
- ((Spannable)action.mSequence).nextSpanTransition(
- -1, Integer.MAX_VALUE, null) < Integer.MAX_VALUE)) {
- // Spans are not preserved when we use key events,
- // so we need the sequence to not have any spans
- return;
- }
- KeyEvent [] keyEvents = synthesizeKeyEvents(action.mSequence);
- if (keyEvents == null) {
- return;
- }
- for (KeyEvent event : keyEvents) {
- if (KeyEvent.isModifierKey(event.getKeyCode())) {
- continue;
- }
- if (event.getAction() == KeyEvent.ACTION_UP && mSuppressKeyUp) {
- continue;
- }
- if (DEBUG) {
- Log.d(LOGTAG, "sending: " + event);
- }
- onKeyEvent(event, event.getAction(),
- /* metaState */ 0, /* isSynthesizedImeKey */ true);
- }
- }
-
- /**
- * Remove the head of the queue. Throw if queue is empty.
- */
- void poll() {
- if (DEBUG) {
- ThreadUtils.assertOnGeckoThread();
- }
- if (mActions.poll() == null) {
- throw new IllegalStateException("empty actions queue");
- }
-
- synchronized (this) {
- if (mActions.isEmpty()) {
- mActionsActive.release();
- }
- }
- }
-
- /**
- * Return, but don't remove, the head of the queue, or null if queue is empty.
- *
- * @return head of the queue or null if empty.
- */
- Action peek() {
- if (DEBUG) {
- ThreadUtils.assertOnGeckoThread();
- }
- return mActions.peek();
- }
-
- void syncWithGecko() {
- if (DEBUG) {
- assertOnIcThread();
- }
- if (mFocused && !mActions.isEmpty()) {
- if (DEBUG) {
- Log.d(LOGTAG, "syncWithGecko blocking on thread " +
- Thread.currentThread().getName());
- }
- mActionsActive.acquireUninterruptibly();
- mActionsActive.release();
- } else if (DEBUG && !mFocused) {
- Log.d(LOGTAG, "skipped syncWithGecko (no focus)");
- }
- }
-
- boolean isEmpty() {
- return mActions.isEmpty();
- }
- }
-
- @WrapForJNI
- GeckoEditable(final GeckoView v) {
- if (DEBUG) {
- // Called by nsWindow.
- ThreadUtils.assertOnGeckoThread();
- }
- mActionQueue = new ActionQueue();
-
- mText = new SpannableStringBuilder();
-
- final Class<?>[] PROXY_INTERFACES = { Editable.class };
- mProxy = (Editable)Proxy.newProxyInstance(
- Editable.class.getClassLoader(),
- PROXY_INTERFACES, this);
-
- mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler();
-
- onViewChange(v);
- }
-
- @WrapForJNI @Override
- protected native void disposeNative();
-
- @WrapForJNI
- /* package */ void onViewChange(final GeckoView v) {
- if (DEBUG) {
- // Called by nsWindow.
- ThreadUtils.assertOnGeckoThread();
- Log.d(LOGTAG, "onViewChange(" + v + ")");
- }
-
- final GeckoEditableListener newListener =
- v != null ? GeckoInputConnection.create(v, this) : null;
-
- final Runnable setListenerRunnable = new Runnable() {
- @Override
- public void run() {
- if (DEBUG) {
- Log.d(LOGTAG, "onViewChange (set listener)");
- }
-
- if (newListener != null) {
- // Make sure there are no other things going on.
- mActionQueue.syncWithGecko();
- mListener = newListener;
- } else {
- // We're being destroyed. By this point, we should have cleared all
- // pending Runnables on the IC thread, so it's safe to call
- // disposeNative here.
- mListener = null;
- GeckoEditable.this.disposeNative();
- }
- }
- };
-
- // Post to UI thread first to make sure any code that is using the old input
- // connection has finished running, before we switch to a new input connection or
- // before we clear the input connection on destruction.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (DEBUG) {
- Log.d(LOGTAG, "onViewChange (set IC)");
- }
-
- if (mView != null) {
- // Detach the previous view.
- mView.setInputConnectionListener(null);
- }
- if (v != null) {
- // And attach the new view.
- v.setInputConnectionListener((InputConnectionListener) newListener);
- }
-
- mView = v;
- mIcPostHandler.post(setListenerRunnable);
- }
- });
- }
-
- private boolean onIcThread() {
- return mIcRunHandler.getLooper() == Looper.myLooper();
- }
-
- private void assertOnIcThread() {
- ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW);
- }
-
- private void geckoPostToIc(Runnable runnable) {
- mIcPostHandler.post(runnable);
- }
-
- private Object getField(Object obj, String field, Object def) {
- try {
- return obj.getClass().getField(field).get(obj);
- } catch (Exception e) {
- return def;
- }
- }
-
- /**
- * Send composition ranges to Gecko if the text has composing spans.
- *
- * @param sequence Text with possible composing spans
- * @param useEntireText If text has composing spans, treat the entire text as
- * a Gecko composition, instead of just the spanned part.
- * @param notifyGecko Notify Gecko of the new composition ranges;
- * otherwise, the caller is responsible for notifying Gecko.
- * @return Whether there was a composition
- */
- private boolean icMaybeSendComposition(final CharSequence sequence,
- final boolean useEntireText,
- final boolean notifyGecko) {
- int selStart = Selection.getSelectionStart(sequence);
- int selEnd = Selection.getSelectionEnd(sequence);
-
- if (sequence instanceof Spanned) {
- final Spanned text = (Spanned) sequence;
- final Object[] spans = text.getSpans(0, text.length(), Object.class);
- boolean found = false;
- int composingStart = useEntireText ? 0 : Integer.MAX_VALUE;
- int composingEnd = useEntireText ? text.length() : 0;
-
- for (Object span : spans) {
- if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) == 0) {
- continue;
- }
- if (!useEntireText) {
- composingStart = Math.min(composingStart, text.getSpanStart(span));
- composingEnd = Math.max(composingEnd, text.getSpanEnd(span));
- }
- found = true;
- }
-
- if (useEntireText && (selStart < 0 || selEnd < 0)) {
- selStart = composingEnd;
- selEnd = composingEnd;
- }
-
- if (found) {
- icSendComposition(text, selStart, selEnd, composingStart, composingEnd);
- if (notifyGecko) {
- mActionQueue.offer(Action.newUpdateComposition(
- composingStart, composingEnd));
- }
- return true;
- }
- }
-
- if (notifyGecko) {
- // Set the selection by using a composition without ranges
- mActionQueue.offer(Action.newUpdateComposition(selStart, selEnd));
- }
-
- if (DEBUG) {
- Log.d(LOGTAG, "icSendComposition(): no composition");
- }
- return false;
- }
-
- private void icSendComposition(final Spanned text,
- final int selStart, final int selEnd,
- final int composingStart, final int composingEnd) {
- if (DEBUG) {
- assertOnIcThread();
- Log.d(LOGTAG, "icSendComposition(\"" + text + "\", " +
- composingStart + ", " + composingEnd + ")");
- }
- if (DEBUG) {
- Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd);
- Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd);
- }
-
- if (selEnd >= composingStart && selEnd <= composingEnd) {
- onImeAddCompositionRange(
- selEnd - composingStart, selEnd - composingStart,
- IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0);
- }
-
- int rangeStart = composingStart;
- TextPaint tp = new TextPaint();
- TextPaint emptyTp = new TextPaint();
- // set initial foreground color to 0, because we check for tp.getColor() == 0
- // below to decide whether to pass a foreground color to Gecko
- emptyTp.setColor(0);
- do {
- int rangeType, rangeStyles = 0, rangeLineStyle = IME_RANGE_LINE_NONE;
- boolean rangeBoldLine = false;
- int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0;
- int rangeEnd = text.nextSpanTransition(rangeStart, composingEnd, Object.class);
-
- if (selStart > rangeStart && selStart < rangeEnd) {
- rangeEnd = selStart;
- } else if (selEnd > rangeStart && selEnd < rangeEnd) {
- rangeEnd = selEnd;
- }
- CharacterStyle[] styleSpans =
- text.getSpans(rangeStart, rangeEnd, CharacterStyle.class);
-
- if (DEBUG) {
- Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " +
- rangeStart + "-" + rangeEnd);
- }
-
- if (styleSpans.length == 0) {
- rangeType = (selStart == rangeStart && selEnd == rangeEnd)
- ? IME_RANGE_SELECTEDRAWTEXT
- : IME_RANGE_RAWINPUT;
- } else {
- rangeType = (selStart == rangeStart && selEnd == rangeEnd)
- ? IME_RANGE_SELECTEDCONVERTEDTEXT
- : IME_RANGE_CONVERTEDTEXT;
- tp.set(emptyTp);
- for (CharacterStyle span : styleSpans) {
- span.updateDrawState(tp);
- }
- int tpUnderlineColor = 0;
- float tpUnderlineThickness = 0.0f;
-
- // These TextPaint fields only exist on Android ICS+ and are not in the SDK.
- if (Versions.feature14Plus) {
- tpUnderlineColor = (Integer)getField(tp, "underlineColor", 0);
- tpUnderlineThickness = (Float)getField(tp, "underlineThickness", 0.0f);
- }
- if (tpUnderlineColor != 0) {
- rangeStyles |= IME_RANGE_UNDERLINE | IME_RANGE_LINECOLOR;
- rangeLineColor = tpUnderlineColor;
- // Approximately translate underline thickness to what Gecko understands
- if (tpUnderlineThickness <= 0.5f) {
- rangeLineStyle = IME_RANGE_LINE_DOTTED;
- } else {
- rangeLineStyle = IME_RANGE_LINE_SOLID;
- if (tpUnderlineThickness >= 2.0f) {
- rangeBoldLine = true;
- }
- }
- } else if (tp.isUnderlineText()) {
- rangeStyles |= IME_RANGE_UNDERLINE;
- rangeLineStyle = IME_RANGE_LINE_SOLID;
- }
- if (tp.getColor() != 0) {
- rangeStyles |= IME_RANGE_FORECOLOR;
- rangeForeColor = tp.getColor();
- }
- if (tp.bgColor != 0) {
- rangeStyles |= IME_RANGE_BACKCOLOR;
- rangeBackColor = tp.bgColor;
- }
- }
- onImeAddCompositionRange(
- rangeStart - composingStart, rangeEnd - composingStart,
- rangeType, rangeStyles, rangeLineStyle, rangeBoldLine,
- rangeForeColor, rangeBackColor, rangeLineColor);
- rangeStart = rangeEnd;
-
- if (DEBUG) {
- Log.d(LOGTAG, " added " + rangeType +
- " : " + Integer.toHexString(rangeStyles) +
- " : " + Integer.toHexString(rangeForeColor) +
- " : " + Integer.toHexString(rangeBackColor));
- }
- } while (rangeStart < composingEnd);
- }
-
- // GeckoEditableClient interface
-
- @Override
- public void sendKeyEvent(final KeyEvent event, int action, int metaState) {
- if (DEBUG) {
- assertOnIcThread();
- Log.d(LOGTAG, "sendKeyEvent(" + event + ", " + action + ", " + metaState + ")");
- }
- /*
- We are actually sending two events to Gecko here,
- 1. Event from the event parameter (key event)
- 2. Sync event from the mActionQueue.offer call
- The first event is a normal event that does not reply back to us,
- the second sync event will have a reply, during which we see that there is a pending
- event-type action, and update the selection/composition/etc. accordingly.
- */
- if (mNeedCompositionUpdate) {
- // Make sure Gecko selection is in-sync with Java selection first.
- icUpdateComposition();
- }
- onKeyEvent(event, action, metaState, /* isSynthesizedImeKey */ false);
- mActionQueue.offer(new Action(Action.TYPE_EVENT));
- }
-
- @Override
- public Editable getEditable() {
- if (!onIcThread()) {
- // Android may be holding an old InputConnection; ignore
- if (DEBUG) {
- Log.i(LOGTAG, "getEditable() called on non-IC thread");
- }
- return null;
- }
- if (mListener == null) {
- // We haven't initialized or we've been destroyed.
- return null;
- }
- return mProxy;
- }
-
- @Override
- public void setBatchMode(boolean inBatchMode) {
- if (!onIcThread()) {
- // Android may be holding an old InputConnection; ignore
- if (DEBUG) {
- Log.i(LOGTAG, "setBatchMode() called on non-IC thread");
- }
- return;
- }
- if (!inBatchMode && mNeedCompositionUpdate) {
- icUpdateComposition();
- }
- mInBatchMode = inBatchMode;
- }
-
- private void icUpdateComposition() {
- if (DEBUG) {
- assertOnIcThread();
- }
- mNeedCompositionUpdate = false;
- mActionQueue.syncWithGecko();
- icMaybeSendComposition(mText, /* useEntireText */ false, /* notifyGecko */ true);
- }
-
- private void geckoScheduleCompositionUpdate() {
- if (DEBUG) {
- ThreadUtils.assertOnGeckoThread();
- }
- // May be called from either Gecko or IC thread
- geckoPostToIc(new Runnable() {
- @Override
- public void run() {
- if (mInBatchMode || !mNeedCompositionUpdate) {
- return;
- }
- if (!mActionQueue.isEmpty()) {
- // We are likely to block here, so wait a little and try again.
- mIcPostHandler.postDelayed(this, 10);
- return;
- }
- icUpdateComposition();
- }
- });
- }
-
- @Override
- public void setSuppressKeyUp(boolean suppress) {
- if (DEBUG) {
- assertOnIcThread();
- }
- // Suppress key up event generated as a result of
- // translating characters to key events
- mSuppressKeyUp = suppress;
- }
-
- @Override
- public Handler setInputConnectionHandler(Handler handler) {
- if (handler == mIcPostHandler || !mFocused) {
- return mIcPostHandler;
- }
- if (DEBUG) {
- assertOnIcThread();
- }
- // There are three threads at this point: Gecko thread, old IC thread, and new IC
- // thread, and we want to safely switch from old IC thread to new IC thread.
- // We first send a TYPE_SET_HANDLER action to the Gecko thread; this ensures that
- // the Gecko thread is stopped at a known point. At the same time, the old IC
- // thread blocks on the action; this ensures that the old IC thread is stopped at
- // a known point. Finally, inside the Gecko thread, we post a Runnable to the old
- // IC thread; this Runnable switches from old IC thread to new IC thread. We
- // switch IC thread on the old IC thread to ensure any pending Runnables on the
- // old IC thread are processed before we switch over. Inside the Gecko thread, we
- // also post a Runnable to the new IC thread; this Runnable blocks until the
- // switch is complete; this ensures that the new IC thread won't accept
- // InputConnection calls until after the switch.
- mActionQueue.offer(Action.newSetHandler(handler));
- mActionQueue.syncWithGecko();
- return handler;
- }
-
- @Override // GeckoEditableClient
- public void postToInputConnection(final Runnable runnable) {
- mIcPostHandler.post(runnable);
- }
-
- private void geckoSetIcHandler(final Handler newHandler) {
- geckoPostToIc(new Runnable() { // posting to old IC thread
- @Override
- public void run() {
- synchronized (newHandler) {
- mIcRunHandler = newHandler;
- newHandler.notify();
- }
- }
- });
-
- // At this point, all future Runnables should be posted to the new IC thread, but
- // we don't switch mIcRunHandler yet because there may be pending Runnables on the
- // old IC thread still waiting to run.
- mIcPostHandler = newHandler;
-
- geckoPostToIc(new Runnable() { // posting to new IC thread
- @Override
- public void run() {
- synchronized (newHandler) {
- while (mIcRunHandler != newHandler) {
- try {
- newHandler.wait();
- } catch (InterruptedException e) {
- }
- }
- }
- }
- });
- }
-
- // GeckoEditableListener interface
-
- private void geckoActionReply() {
- if (DEBUG) {
- // GeckoEditableListener methods should all be called from the Gecko thread
- ThreadUtils.assertOnGeckoThread();
- }
-
- final Action action = mActionQueue.peek();
- if (action == null) {
- throw new IllegalStateException("empty actions queue");
- }
-
- if (DEBUG) {
- Log.d(LOGTAG, "reply: Action(" +
- getConstantName(Action.class, "TYPE_", action.mType) + ")");
- }
- switch (action.mType) {
- case Action.TYPE_SET_SPAN:
- mText.setSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags);
- break;
-
- case Action.TYPE_REMOVE_SPAN:
- mText.removeSpan(action.mSpanObject);
- break;
-
- case Action.TYPE_SET_HANDLER:
- geckoSetIcHandler(action.mHandler);
- break;
- }
-
- if (action.mUpdateComposition) {
- geckoScheduleCompositionUpdate();
- }
- }
-
- private void notifyCommitComposition() {
- // Gecko already committed its composition. However, Android keyboards
- // have trouble dealing with us removing the composition manually on
- // the Java side. Therefore, we keep the composition intact on the Java
- // side. The text content should still be in-sync on both sides.
- }
-
- private void notifyCancelComposition() {
- // Composition should have been canceled on our side
- // through text update notifications; verify that here.
- if (DEBUG) {
- final Object[] spans = mText.getSpans(0, mText.length(), Object.class);
- for (Object span : spans) {
- if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
- throw new IllegalStateException("composition not cancelled");
- }
- }
- }
- }
-
- @WrapForJNI @Override
- public void notifyIME(final int type) {
- if (DEBUG) {
- // GeckoEditableListener methods should all be called from the Gecko thread
- ThreadUtils.assertOnGeckoThread();
- // NOTIFY_IME_REPLY_EVENT is logged separately, inside geckoActionReply()
- if (type != NOTIFY_IME_REPLY_EVENT) {
- Log.d(LOGTAG, "notifyIME(" +
- getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) +
- ")");
- }
- }
-
- if (type == NOTIFY_IME_REPLY_EVENT) {
- try {
- if (mGeckoFocused) {
- // When mGeckoFocused is false, the reply is for a stale action,
- // and we should not do anything
- geckoActionReply();
- } else if (DEBUG) {
- Log.d(LOGTAG, "discarding stale reply");
- }
- } finally {
- // Ensure action is always removed from queue
- // even if stale action results in exception in geckoActionReply
- mActionQueue.poll();
- }
- return;
- } else if (type == NOTIFY_IME_TO_COMMIT_COMPOSITION) {
- notifyCommitComposition();
- return;
- } else if (type == NOTIFY_IME_TO_CANCEL_COMPOSITION) {
- notifyCancelComposition();
- return;
- }
-
- geckoPostToIc(new Runnable() {
- @Override
- public void run() {
- if (type == NOTIFY_IME_OF_FOCUS) {
- mFocused = true;
- // Unmask events on the Gecko side
- mActionQueue.offer(new Action(Action.TYPE_ACKNOWLEDGE_FOCUS));
- }
-
- // Make sure there are no other things going on. If we sent
- // Action.TYPE_ACKNOWLEDGE_FOCUS, this line also makes us
- // wait for Gecko to update us on the newly focused content
- mActionQueue.syncWithGecko();
- if (mListener != null) {
- mListener.notifyIME(type);
- }
-
- // Unset mFocused after we call syncWithGecko because
- // syncWithGecko becomes a no-op when mFocused is false.
- if (type == NOTIFY_IME_OF_BLUR) {
- mFocused = false;
- }
- }
- });
-
- // Register/unregister Gecko-side text selection listeners
- // and update the mGeckoFocused flag.
- if (type == NOTIFY_IME_OF_BLUR && mGeckoFocused) {
- // Check for focus here because Gecko may send us a blur before a focus in some
- // cases, and we don't want to unregister an event that was not registered.
- mGeckoFocused = false;
- mSuppressCompositions = false;
- EventDispatcher.getInstance().
- unregisterGeckoThreadListener(this, "TextSelection:DraggingHandle");
- } else if (type == NOTIFY_IME_OF_FOCUS) {
- mGeckoFocused = true;
- mSuppressCompositions = false;
- EventDispatcher.getInstance().
- registerGeckoThreadListener(this, "TextSelection:DraggingHandle");
- }
- }
-
- @WrapForJNI @Override
- public void notifyIMEContext(final int state, final String typeHint,
- final String modeHint, final String actionHint) {
- if (DEBUG) {
- // GeckoEditableListener methods should all be called from the Gecko thread
- ThreadUtils.assertOnGeckoThread();
- Log.d(LOGTAG, "notifyIMEContext(" +
- getConstantName(GeckoEditableListener.class, "IME_STATE_", state) +
- ", \"" + typeHint + "\", \"" + modeHint + "\", \"" + actionHint + "\")");
- }
- geckoPostToIc(new Runnable() {
- @Override
- public void run() {
- if (mListener == null) {
- return;
- }
- mListener.notifyIMEContext(state, typeHint, modeHint, actionHint);
- }
- });
- }
-
- @WrapForJNI @Override
- public void onSelectionChange(int start, int end) {
- if (DEBUG) {
- // GeckoEditableListener methods should all be called from the Gecko thread
- ThreadUtils.assertOnGeckoThread();
- Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")");
- }
- if (start < 0 || start > mText.length() || end < 0 || end > mText.length()) {
- Log.e(LOGTAG, "invalid selection notification range: " +
- start + " to " + end + ", length: " + mText.length());
- throw new IllegalArgumentException("invalid selection notification range");
- }
-
- if (mIgnoreSelectionChange) {
- start = Selection.getSelectionStart(mText);
- end = Selection.getSelectionEnd(mText);
- mIgnoreSelectionChange = false;
-
- } else {
- Selection.setSelection(mText, start, end);
- }
-
- final int newStart = start;
- final int newEnd = end;
- geckoPostToIc(new Runnable() {
- @Override
- public void run() {
- if (mListener == null) {
- return;
- }
- mListener.onSelectionChange(newStart, newEnd);
- }
- });
- }
-
- private void geckoReplaceText(int start, int oldEnd, CharSequence newText) {
- mText.replace(start, oldEnd, newText);
- }
-
- private boolean geckoIsSameText(int start, int oldEnd, CharSequence newText) {
- return oldEnd - start == newText.length() &&
- TextUtils.regionMatches(mText, start, newText, 0, oldEnd - start);
- }
-
- @WrapForJNI @Override
- public void onTextChange(final CharSequence text, final int start,
- final int unboundedOldEnd, final int unboundedNewEnd) {
- if (DEBUG) {
- // GeckoEditableListener methods should all be called from the Gecko thread
- ThreadUtils.assertOnGeckoThread();
- StringBuilder sb = new StringBuilder("onTextChange(");
- debugAppend(sb, text);
- sb.append(", ").append(start).append(", ")
- .append(unboundedOldEnd).append(", ")
- .append(unboundedNewEnd).append(")");
- Log.d(LOGTAG, sb.toString());
- }
- if (start < 0 || start > unboundedOldEnd) {
- Log.e(LOGTAG, "invalid text notification range: " +
- start + " to " + unboundedOldEnd);
- throw new IllegalArgumentException("invalid text notification range");
- }
- /* For the "end" parameters, Gecko can pass in a large
- number to denote "end of the text". Fix that here */
- final int oldEnd = unboundedOldEnd > mText.length() ? mText.length() : unboundedOldEnd;
- // new end should always match text
- if (unboundedOldEnd <= mText.length() &&
- unboundedNewEnd != (start + text.length())) {
- Log.e(LOGTAG, "newEnd does not match text: " + unboundedNewEnd + " vs " +
- (start + text.length()));
- throw new IllegalArgumentException("newEnd does not match text");
- }
- final int newEnd = start + text.length();
- final Action action = mActionQueue.peek();
-
- if (action != null && action.mType == Action.TYPE_ACKNOWLEDGE_FOCUS) {
- // Simply replace the text for newly-focused editors.
- mText.replace(0, mText.length(), text);
-
- } else if (action != null &&
- action.mType == Action.TYPE_REPLACE_TEXT &&
- start <= action.mStart &&
- oldEnd >= action.mEnd &&
- newEnd >= action.mStart + action.mSequence.length()) {
-
- // Try to preserve both old spans and new spans in action.mSequence.
- // indexInText is where we can find waction.mSequence within the passed in text.
- final int startWithinText = action.mStart - start;
- int indexInText = TextUtils.indexOf(text, action.mSequence, startWithinText);
- if (indexInText < 0 && startWithinText >= action.mSequence.length()) {
- indexInText = text.toString().lastIndexOf(action.mSequence.toString(),
- startWithinText);
- }
-
- if (indexInText < 0) {
- // Text was changed from under us. We are forced to discard any new spans.
- geckoReplaceText(start, oldEnd, text);
-
- // Don't ignore the next selection change because we are forced to re-sync
- // with Gecko here.
- mIgnoreSelectionChange = false;
-
- } else if (indexInText == 0 && text.length() == action.mSequence.length()) {
- // The new text exactly matches our sequence, so do a direct replace.
- geckoReplaceText(start, oldEnd, action.mSequence);
-
- // Ignore the next selection change because the selection change is a
- // side-effect of the replace-text event we sent.
- mIgnoreSelectionChange = true;
-
- } else {
- // The sequence is embedded within the changed text, so we have to perform
- // replacement in parts. First replace part of text before the sequence.
- geckoReplaceText(start, action.mStart, text.subSequence(0, indexInText));
-
- // Then Replace the sequence itself to preserve new spans.
- final int actionStart = indexInText + start;
- geckoReplaceText(actionStart, actionStart + action.mEnd - action.mStart,
- action.mSequence);
-
- // Finally replace part of text after the sequence.
- final int actionEnd = actionStart + action.mSequence.length();
- geckoReplaceText(actionEnd, actionEnd + oldEnd - action.mEnd,
- text.subSequence(actionEnd - start, text.length()));
- }
-
- } else if (geckoIsSameText(start, oldEnd, text)) {
- // Nothing to do because the text is the same. This could happen when
- // the composition is updated for example, in which case we want to keep the
- // Java selection.
- mIgnoreSelectionChange = mIgnoreSelectionChange ||
- (action != null && action.mType == Action.TYPE_UPDATE_COMPOSITION);
- return;
-
- } else {
- // Gecko side initiated the text change.
- geckoReplaceText(start, oldEnd, text);
- }
-
- geckoPostToIc(new Runnable() {
- @Override
- public void run() {
- if (mListener == null) {
- return;
- }
- mListener.onTextChange(text, start, oldEnd, newEnd);
- }
- });
- }
-
- @WrapForJNI @Override
- public void onDefaultKeyEvent(final KeyEvent event) {
- if (DEBUG) {
- // GeckoEditableListener methods should all be called from the Gecko thread
- ThreadUtils.assertOnGeckoThread();
- StringBuilder sb = new StringBuilder("onDefaultKeyEvent(");
- sb.append("action=").append(event.getAction()).append(", ")
- .append("keyCode=").append(event.getKeyCode()).append(", ")
- .append("metaState=").append(event.getMetaState()).append(", ")
- .append("time=").append(event.getEventTime()).append(", ")
- .append("repeatCount=").append(event.getRepeatCount()).append(")");
- Log.d(LOGTAG, sb.toString());
- }
-
- geckoPostToIc(new Runnable() {
- @Override
- public void run() {
- if (mListener == null) {
- return;
- }
- mListener.onDefaultKeyEvent(event);
- }
- });
- }
-
- // InvocationHandler interface
-
- static String getConstantName(Class<?> cls, String prefix, Object value) {
- for (Field fld : cls.getDeclaredFields()) {
- try {
- if (fld.getName().startsWith(prefix) &&
- fld.get(null).equals(value)) {
- return fld.getName();
- }
- } catch (IllegalAccessException e) {
- }
- }
- return String.valueOf(value);
- }
-
- static StringBuilder debugAppend(StringBuilder sb, Object obj) {
- if (obj == null) {
- sb.append("null");
- } else if (obj instanceof GeckoEditable) {
- sb.append("GeckoEditable");
- } else if (Proxy.isProxyClass(obj.getClass())) {
- debugAppend(sb, Proxy.getInvocationHandler(obj));
- } else if (obj instanceof CharSequence) {
- sb.append('"').append(obj.toString().replace('\n', '\u21b2')).append('"');
- } else if (obj.getClass().isArray()) {
- sb.append(obj.getClass().getComponentType().getSimpleName()).append('[')
- .append(Array.getLength(obj)).append(']');
- } else {
- sb.append(obj);
- }
- return sb;
- }
-
- @Override
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- Object target;
- final Class<?> methodInterface = method.getDeclaringClass();
- if (DEBUG) {
- // Editable methods should all be called from the IC thread
- assertOnIcThread();
- }
- if (methodInterface == Editable.class ||
- methodInterface == Appendable.class ||
- methodInterface == Spannable.class) {
- // Method alters the Editable; route calls to our implementation
- target = this;
- } else {
- // Method queries the Editable; must sync with Gecko first
- // then call on the inner Editable itself
- mActionQueue.syncWithGecko();
- target = mText;
- }
- Object ret;
- try {
- ret = method.invoke(target, args);
- } catch (InvocationTargetException e) {
- // Bug 817386
- // Most likely Gecko has changed the text while GeckoInputConnection is
- // trying to access the text. If we pass through the exception here, Fennec
- // will crash due to a lack of exception handler. Log the exception and
- // return an empty value instead.
- if (!(e.getCause() instanceof IndexOutOfBoundsException)) {
- // Only handle IndexOutOfBoundsException for now,
- // as other exceptions might signal other bugs
- throw e;
- }
- Log.w(LOGTAG, "Exception in GeckoEditable." + method.getName(), e.getCause());
- Class<?> retClass = method.getReturnType();
- if (retClass == Character.TYPE) {
- ret = '\0';
- } else if (retClass == Integer.TYPE) {
- ret = 0;
- } else if (retClass == String.class) {
- ret = "";
- } else {
- ret = null;
- }
- }
- if (DEBUG) {
- StringBuilder log = new StringBuilder(method.getName());
- log.append("(");
- if (args != null) {
- for (Object arg : args) {
- debugAppend(log, arg).append(", ");
- }
- if (args.length > 0) {
- log.setLength(log.length() - 2);
- }
- }
- if (method.getReturnType().equals(Void.TYPE)) {
- log.append(")");
- } else {
- debugAppend(log.append(") = "), ret);
- }
- Log.d(LOGTAG, log.toString());
- }
- return ret;
- }
-
- // Spannable interface
-
- @Override
- public void removeSpan(Object what) {
- if (what == Selection.SELECTION_START ||
- what == Selection.SELECTION_END) {
- Log.w(LOGTAG, "selection removed with removeSpan()");
- }
-
- // "what" could be a composing span (BaseInputConnection.COMPOSING extends
- // NoCopySpan), so we should update the Gecko composition
- final boolean update = !mNeedCompositionUpdate && what instanceof NoCopySpan;
- mActionQueue.offer(Action.newRemoveSpan(what, update));
- mNeedCompositionUpdate |= update;
- }
-
- @Override
- public void setSpan(Object what, int start, int end, int flags) {
- // If mUpdateComposition is true, it means something in the queue will be
- // scheduling an update, so it would be redundant to have this action schedule
- // another update.
- final boolean update = !mNeedCompositionUpdate &&
- (flags & Spanned.SPAN_INTERMEDIATE) == 0 && (
- (flags & Spanned.SPAN_COMPOSING) != 0 ||
- what == Selection.SELECTION_START ||
- what == Selection.SELECTION_END);
- mActionQueue.offer(Action.newSetSpan(what, start, end, flags, update));
- mNeedCompositionUpdate |= update;
- }
-
- // Appendable interface
-
- @Override
- public Editable append(CharSequence text) {
- return replace(mProxy.length(), mProxy.length(), text, 0, text.length());
- }
-
- @Override
- public Editable append(CharSequence text, int start, int end) {
- return replace(mProxy.length(), mProxy.length(), text, start, end);
- }
-
- @Override
- public Editable append(char text) {
- return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1);
- }
-
- // Editable interface
-
- @Override
- public InputFilter[] getFilters() {
- return mFilters;
- }
-
- @Override
- public void setFilters(InputFilter[] filters) {
- mFilters = filters;
- }
-
- @Override
- public void clearSpans() {
- /* XXX this clears the selection spans too,
- but there is no way to clear the corresponding selection in Gecko */
- Log.w(LOGTAG, "selection cleared with clearSpans()");
- mText.clearSpans();
- }
-
- @Override
- public Editable replace(int st, int en,
- CharSequence source, int start, int end) {
-
- CharSequence text = source;
- if (start < 0 || start > end || end > text.length()) {
- Log.e(LOGTAG, "invalid replace offsets: " +
- start + " to " + end + ", length: " + text.length());
- throw new IllegalArgumentException("invalid replace offsets");
- }
- if (start != 0 || end != text.length()) {
- text = text.subSequence(start, end);
- }
- if (mFilters != null) {
- // Filter text before sending the request to Gecko
- for (int i = 0; i < mFilters.length; ++i) {
- final CharSequence cs = mFilters[i].filter(
- text, 0, text.length(), mProxy, st, en);
- if (cs != null) {
- text = cs;
- }
- }
- }
- if (text == source) {
- // Always create a copy
- text = new SpannableString(source);
- }
- mActionQueue.offer(Action.newReplaceText(text,
- Math.min(st, en), Math.max(st, en)));
- return mProxy;
- }
-
- @Override
- public void clear() {
- replace(0, mProxy.length(), "", 0, 0);
- }
-
- @Override
- public Editable delete(int st, int en) {
- return replace(st, en, "", 0, 0);
- }
-
- @Override
- public Editable insert(int where, CharSequence text,
- int start, int end) {
- return replace(where, where, text, start, end);
- }
-
- @Override
- public Editable insert(int where, CharSequence text) {
- return replace(where, where, text, 0, text.length());
- }
-
- @Override
- public Editable replace(int st, int en, CharSequence text) {
- return replace(st, en, text, 0, text.length());
- }
-
- /* GetChars interface */
-
- @Override
- public void getChars(int start, int end, char[] dest, int destoff) {
- /* overridden Editable interface methods in GeckoEditable must not be called directly
- outside of GeckoEditable. Instead, the call must go through mProxy, which ensures
- that Java is properly synchronized with Gecko */
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- /* Spanned interface */
-
- @Override
- public int getSpanEnd(Object tag) {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- @Override
- public int getSpanFlags(Object tag) {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- @Override
- public int getSpanStart(Object tag) {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- @Override
- public <T> T[] getSpans(int start, int end, Class<T> type) {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- @Override
- @SuppressWarnings("rawtypes") // nextSpanTransition uses raw Class in its Android declaration
- public int nextSpanTransition(int start, int limit, Class type) {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- /* CharSequence interface */
-
- @Override
- public char charAt(int index) {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- @Override
- public int length() {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- @Override
- public CharSequence subSequence(int start, int end) {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- @Override
- public String toString() {
- throw new UnsupportedOperationException("method must be called through mProxy");
- }
-
- // GeckoEventListener implementation
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- if (!"TextSelection:DraggingHandle".equals(event)) {
- return;
- }
-
- mSuppressCompositions = message.optBoolean("dragging", false);
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEditableClient.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/* -*- 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.Handler;
-import android.text.Editable;
-import android.view.KeyEvent;
-
-/**
- * Interface for the IC thread.
- */
-interface GeckoEditableClient {
- void sendKeyEvent(KeyEvent event, int action, int metaState);
- Editable getEditable();
- void setBatchMode(boolean isBatchMode);
- void setSuppressKeyUp(boolean suppress);
- Handler setInputConnectionHandler(Handler handler);
- void postToInputConnection(Runnable runnable);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEditableListener.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/* -*- 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 android.view.KeyEvent;
-
-/**
- * Interface for the Editable to listen on the Gecko thread, as well as for the IC thread to listen
- * to the Editable.
- */
-interface GeckoEditableListener {
- // IME notification type for notifyIME(), corresponding to NotificationToIME enum in Gecko
- @WrapForJNI
- int NOTIFY_IME_OPEN_VKB = -2;
- @WrapForJNI
- int NOTIFY_IME_REPLY_EVENT = -1;
- @WrapForJNI
- int NOTIFY_IME_OF_FOCUS = 1;
- @WrapForJNI
- int NOTIFY_IME_OF_BLUR = 2;
- @WrapForJNI
- int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8;
- @WrapForJNI
- int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
- // IME enabled state for notifyIMEContext()
- int IME_STATE_DISABLED = 0;
- int IME_STATE_ENABLED = 1;
- int IME_STATE_PASSWORD = 2;
- int IME_STATE_PLUGIN = 3;
-
- void notifyIME(int type);
- void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint);
- void onSelectionChange(int start, int end);
- void onTextChange(CharSequence text, int start, int oldEnd, int newEnd);
- void onDefaultKeyEvent(KeyEvent event);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEvent.java
+++ /dev/null
@@ -1,634 +0,0 @@
-/* -*- 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.nio.ByteBuffer;
-import java.util.concurrent.ArrayBlockingQueue;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.gfx.DisplayPortMetrics;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
-
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorManager;
-import android.location.Address;
-import android.location.Location;
-import android.os.SystemClock;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-/**
- * We're not allowed to hold on to most events given to us
- * so we save the parts of the events we want to use in GeckoEvent.
- * Fields have different meanings depending on the event type.
- */
-@JNITarget
-public class GeckoEvent {
- private static final String LOGTAG = "GeckoEvent";
-
- private static final int EVENT_FACTORY_SIZE = 5;
-
- // Maybe we're probably better to just make mType non final, and just store GeckoEvents in here...
- private static final SparseArray<ArrayBlockingQueue<GeckoEvent>> mEvents = new SparseArray<ArrayBlockingQueue<GeckoEvent>>();
-
- public static GeckoEvent get(NativeGeckoEvent type) {
- synchronized (mEvents) {
- ArrayBlockingQueue<GeckoEvent> events = mEvents.get(type.value);
- if (events != null && events.size() > 0) {
- return events.poll();
- }
- }
-
- return new GeckoEvent(type);
- }
-
- public void recycle() {
- synchronized (mEvents) {
- ArrayBlockingQueue<GeckoEvent> events = mEvents.get(mType);
- if (events == null) {
- events = new ArrayBlockingQueue<GeckoEvent>(EVENT_FACTORY_SIZE);
- mEvents.put(mType, events);
- }
-
- events.offer(this);
- }
- }
-
- // Make sure to keep these values in sync with the enum in
- // AndroidGeckoEvent in widget/android/AndroidJavaWrappers.h
- @JNITarget
- public enum NativeGeckoEvent {
- NATIVE_POKE(0),
- MOTION_EVENT(2),
- SENSOR_EVENT(3),
- LOCATION_EVENT(5),
- LOAD_URI(12),
- NOOP(15),
- VIEWPORT(20),
- VISITED(21),
- NETWORK_CHANGED(22),
- THUMBNAIL(25),
- SCREENORIENTATION_CHANGED(27),
- NATIVE_GESTURE_EVENT(31),
- CALL_OBSERVER(33),
- REMOVE_OBSERVER(34),
- LOW_MEMORY(35),
- NETWORK_LINK_CHANGE(36),
- TELEMETRY_HISTOGRAM_ADD(37),
- TELEMETRY_UI_SESSION_START(42),
- TELEMETRY_UI_SESSION_STOP(43),
- TELEMETRY_UI_EVENT(44),
- GAMEPAD_ADDREMOVE(45),
- GAMEPAD_DATA(46),
- LONG_PRESS(47),
- ZOOMEDVIEW(48);
-
- public final int value;
-
- private NativeGeckoEvent(int value) {
- this.value = value;
- }
- }
-
- public static final int ACTION_MAGNIFY_START = 11;
- public static final int ACTION_MAGNIFY = 12;
- public static final int ACTION_MAGNIFY_END = 13;
-
- public static final int ACTION_GAMEPAD_ADDED = 1;
- public static final int ACTION_GAMEPAD_REMOVED = 2;
-
- public static final int ACTION_GAMEPAD_BUTTON = 1;
- public static final int ACTION_GAMEPAD_AXES = 2;
-
- private final int mType;
- private int mAction;
- private long mTime;
- private Point[] mPoints;
- private int[] mPointIndicies;
- private int mPointerIndex; // index of the point that has changed
- private float[] mOrientations;
- private float[] mPressures;
- private int[] mToolTypes;
- private Point[] mPointRadii;
- private Rect mRect;
- private double mX;
- private double mY;
- private double mZ;
- private double mW;
-
- private int mMetaState;
- private int mFlags;
- private int mCount;
- private String mCharacters;
- private String mCharactersExtra;
- private String mData;
- private Location mLocation;
-
- private int mConnectionType;
- private boolean mIsWifi;
- private int mDHCPGateway;
-
- private short mScreenOrientation;
- private short mScreenAngle;
-
- private ByteBuffer mBuffer;
-
- private int mWidth;
- private int mHeight;
-
- private int mID;
- private int mGamepadButton;
- private boolean mGamepadButtonPressed;
- private float mGamepadButtonValue;
- private float[] mGamepadValues;
-
- private GeckoEvent(NativeGeckoEvent event) {
- mType = event.value;
- }
-
- public static GeckoEvent createNoOpEvent() {
- return GeckoEvent.get(NativeGeckoEvent.NOOP);
- }
-
- /**
- * This method is a replacement for the the KeyEvent.isGamepadButton method to be
- * compatible with Build.VERSION.SDK_INT < 12. This is an implementation of the
- * same method isGamepadButton available after SDK 12.
- * @param keyCode int with the key code (Android key constant from KeyEvent).
- * @return True if the keycode is a gamepad button, such as {@link #KEYCODE_BUTTON_A}.
- */
- private static boolean isGamepadButton(int keyCode) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BUTTON_A:
- case KeyEvent.KEYCODE_BUTTON_B:
- case KeyEvent.KEYCODE_BUTTON_C:
- case KeyEvent.KEYCODE_BUTTON_X:
- case KeyEvent.KEYCODE_BUTTON_Y:
- case KeyEvent.KEYCODE_BUTTON_Z:
- case KeyEvent.KEYCODE_BUTTON_L1:
- case KeyEvent.KEYCODE_BUTTON_R1:
- case KeyEvent.KEYCODE_BUTTON_L2:
- case KeyEvent.KEYCODE_BUTTON_R2:
- case KeyEvent.KEYCODE_BUTTON_THUMBL:
- case KeyEvent.KEYCODE_BUTTON_THUMBR:
- case KeyEvent.KEYCODE_BUTTON_START:
- case KeyEvent.KEYCODE_BUTTON_SELECT:
- case KeyEvent.KEYCODE_BUTTON_MODE:
- case KeyEvent.KEYCODE_BUTTON_1:
- case KeyEvent.KEYCODE_BUTTON_2:
- case KeyEvent.KEYCODE_BUTTON_3:
- case KeyEvent.KEYCODE_BUTTON_4:
- case KeyEvent.KEYCODE_BUTTON_5:
- case KeyEvent.KEYCODE_BUTTON_6:
- case KeyEvent.KEYCODE_BUTTON_7:
- case KeyEvent.KEYCODE_BUTTON_8:
- case KeyEvent.KEYCODE_BUTTON_9:
- case KeyEvent.KEYCODE_BUTTON_10:
- case KeyEvent.KEYCODE_BUTTON_11:
- case KeyEvent.KEYCODE_BUTTON_12:
- case KeyEvent.KEYCODE_BUTTON_13:
- case KeyEvent.KEYCODE_BUTTON_14:
- case KeyEvent.KEYCODE_BUTTON_15:
- case KeyEvent.KEYCODE_BUTTON_16:
- return true;
- default:
- return false;
- }
- }
-
- public static GeckoEvent createNativeGestureEvent(int action, PointF pt, double size) {
- try {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NATIVE_GESTURE_EVENT);
- event.mAction = action;
- event.mCount = 1;
- event.mPoints = new Point[1];
-
- PointF geckoPoint = new PointF(pt.x, pt.y);
- geckoPoint = GeckoAppShell.getLayerView().convertViewPointToLayerPoint(geckoPoint);
-
- if (geckoPoint == null) {
- // This could happen if Gecko isn't ready yet.
- return null;
- }
-
- event.mPoints[0] = new Point((int)Math.floor(geckoPoint.x), (int)Math.floor(geckoPoint.y));
-
- event.mX = size;
- event.mTime = System.currentTimeMillis();
- return event;
- } catch (Exception e) {
- // This can happen if Gecko isn't ready yet
- return null;
- }
- }
-
- /**
- * Creates a GeckoEvent that contains the data from the MotionEvent.
- * The keepInViewCoordinates parameter can be set to false to convert from the Java
- * coordinate system (device pixels relative to the LayerView) to a coordinate system
- * relative to gecko's coordinate system (CSS pixels relative to gecko scroll position).
- */
- public static GeckoEvent createMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.MOTION_EVENT);
- event.initMotionEvent(m, keepInViewCoordinates);
- return event;
- }
-
- /**
- * Creates a GeckoEvent that contains the data from the LongPressEvent, to be
- * dispatched in CSS pixels relative to gecko's scroll position.
- */
- public static GeckoEvent createLongPressEvent(MotionEvent m) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LONG_PRESS);
- event.initMotionEvent(m, false);
- return event;
- }
-
- private void initMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
- mAction = m.getActionMasked();
- mTime = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + m.getEventTime();
- mMetaState = m.getMetaState();
-
- switch (mAction) {
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_POINTER_UP:
- case MotionEvent.ACTION_POINTER_DOWN:
- case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_MOVE:
- case MotionEvent.ACTION_HOVER_ENTER:
- case MotionEvent.ACTION_HOVER_MOVE:
- case MotionEvent.ACTION_HOVER_EXIT: {
- mCount = m.getPointerCount();
- mPoints = new Point[mCount];
- mPointIndicies = new int[mCount];
- mOrientations = new float[mCount];
- mPressures = new float[mCount];
- mToolTypes = new int[mCount];
- mPointRadii = new Point[mCount];
- mPointerIndex = m.getActionIndex();
- for (int i = 0; i < mCount; i++) {
- addMotionPoint(i, i, m, keepInViewCoordinates);
- }
- break;
- }
- default: {
- mCount = 0;
- mPointerIndex = -1;
- mPoints = new Point[mCount];
- mPointIndicies = new int[mCount];
- mOrientations = new float[mCount];
- mPressures = new float[mCount];
- mToolTypes = new int[mCount];
- mPointRadii = new Point[mCount];
- }
- }
- }
-
- private void addMotionPoint(int index, int eventIndex, MotionEvent event, boolean keepInViewCoordinates) {
- try {
- PointF geckoPoint = new PointF(event.getX(eventIndex), event.getY(eventIndex));
- if (!keepInViewCoordinates) {
- geckoPoint = GeckoAppShell.getLayerView().convertViewPointToLayerPoint(geckoPoint);
- }
-
- mPoints[index] = new Point((int)Math.floor(geckoPoint.x), (int)Math.floor(geckoPoint.y));
- mPointIndicies[index] = event.getPointerId(eventIndex);
-
- double radians = event.getOrientation(eventIndex);
- mOrientations[index] = (float) Math.toDegrees(radians);
- // w3c touchevents spec does not allow orientations == 90
- // this shifts it to -90, which will be shifted to zero below
- if (mOrientations[index] == 90)
- mOrientations[index] = -90;
-
- // w3c touchevent radius are given by an orientation between 0 and 90
- // the radius is found by removing the orientation and measuring the x and y
- // radius of the resulting ellipse
- // for android orientations >= 0 and < 90, the major axis should correspond to
- // just reporting the y radius as the major one, and x as minor
- // however, for a radius < 0, we have to shift the orientation by adding 90, and
- // reverse which radius is major and minor
- if (mOrientations[index] < 0) {
- mOrientations[index] += 90;
- mPointRadii[index] = new Point((int) event.getToolMajor(eventIndex) / 2,
- (int) event.getToolMinor(eventIndex) / 2);
- } else {
- mPointRadii[index] = new Point((int) event.getToolMinor(eventIndex) / 2,
- (int) event.getToolMajor(eventIndex) / 2);
- }
-
- if (!keepInViewCoordinates) {
- // If we are converting to gecko CSS pixels, then we should adjust the
- // radii as well
- float zoom = GeckoAppShell.getLayerView().getViewportMetrics().zoomFactor;
- mPointRadii[index].x /= zoom;
- mPointRadii[index].y /= zoom;
- }
- mPressures[index] = event.getPressure(eventIndex);
- if (Versions.feature14Plus) {
- mToolTypes[index] = event.getToolType(index);
- }
- } catch (Exception ex) {
- Log.e(LOGTAG, "Error creating motion point " + index, ex);
- mPointRadii[index] = new Point(0, 0);
- mPoints[index] = new Point(0, 0);
- }
- }
-
- private static int HalSensorAccuracyFor(int androidAccuracy) {
- switch (androidAccuracy) {
- case SensorManager.SENSOR_STATUS_UNRELIABLE:
- return GeckoHalDefines.SENSOR_ACCURACY_UNRELIABLE;
- case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
- return GeckoHalDefines.SENSOR_ACCURACY_LOW;
- case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
- return GeckoHalDefines.SENSOR_ACCURACY_MED;
- case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
- return GeckoHalDefines.SENSOR_ACCURACY_HIGH;
- }
- return GeckoHalDefines.SENSOR_ACCURACY_UNKNOWN;
- }
-
- public static GeckoEvent createSensorEvent(SensorEvent s) {
- int sensor_type = s.sensor.getType();
- GeckoEvent event = null;
-
- switch (sensor_type) {
-
- case Sensor.TYPE_ACCELEROMETER:
- event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
- event.mFlags = GeckoHalDefines.SENSOR_ACCELERATION;
- event.mMetaState = HalSensorAccuracyFor(s.accuracy);
- event.mX = s.values[0];
- event.mY = s.values[1];
- event.mZ = s.values[2];
- break;
-
- case Sensor.TYPE_LINEAR_ACCELERATION:
- event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
- event.mFlags = GeckoHalDefines.SENSOR_LINEAR_ACCELERATION;
- event.mMetaState = HalSensorAccuracyFor(s.accuracy);
- event.mX = s.values[0];
- event.mY = s.values[1];
- event.mZ = s.values[2];
- break;
-
- case Sensor.TYPE_ORIENTATION:
- event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
- event.mFlags = GeckoHalDefines.SENSOR_ORIENTATION;
- event.mMetaState = HalSensorAccuracyFor(s.accuracy);
- event.mX = s.values[0];
- event.mY = s.values[1];
- event.mZ = s.values[2];
- break;
-
- case Sensor.TYPE_GYROSCOPE:
- event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
- event.mFlags = GeckoHalDefines.SENSOR_GYROSCOPE;
- event.mMetaState = HalSensorAccuracyFor(s.accuracy);
- event.mX = Math.toDegrees(s.values[0]);
- event.mY = Math.toDegrees(s.values[1]);
- event.mZ = Math.toDegrees(s.values[2]);
- break;
-
- case Sensor.TYPE_PROXIMITY:
- event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
- event.mFlags = GeckoHalDefines.SENSOR_PROXIMITY;
- event.mMetaState = HalSensorAccuracyFor(s.accuracy);
- event.mX = s.values[0];
- event.mY = 0;
- event.mZ = s.sensor.getMaximumRange();
- break;
-
- case Sensor.TYPE_LIGHT:
- event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
- event.mFlags = GeckoHalDefines.SENSOR_LIGHT;
- event.mMetaState = HalSensorAccuracyFor(s.accuracy);
- event.mX = s.values[0];
- break;
-
- case Sensor.TYPE_ROTATION_VECTOR:
- case Sensor.TYPE_GAME_ROTATION_VECTOR: // API >= 18
- event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
- event.mFlags = (sensor_type == Sensor.TYPE_ROTATION_VECTOR ?
- GeckoHalDefines.SENSOR_ROTATION_VECTOR :
- GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR);
- event.mMetaState = HalSensorAccuracyFor(s.accuracy);
- event.mX = s.values[0];
- event.mY = s.values[1];
- event.mZ = s.values[2];
- if (s.values.length >= 4) {
- event.mW = s.values[3];
- } else {
- // s.values[3] was optional in API <= 18, so we need to compute it
- // The values form a unit quaternion, so we can compute the angle of
- // rotation purely based on the given 3 values.
- event.mW = 1 - s.values[0] * s.values[0] - s.values[1] * s.values[1] - s.values[2] * s.values[2];
- event.mW = (event.mW > 0.0) ? Math.sqrt(event.mW) : 0.0;
- }
- break;
- }
-
- // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds.
- event.mTime = s.timestamp / 1000;
- return event;
- }
-
- public static GeckoEvent createLocationEvent(Location l) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOCATION_EVENT);
- event.mLocation = l;
- return event;
- }
-
- public static GeckoEvent createViewportEvent(ImmutableViewportMetrics metrics, DisplayPortMetrics displayPort) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.VIEWPORT);
- event.mCharacters = "Viewport:Change";
- StringBuilder sb = new StringBuilder(256);
- sb.append("{ \"x\" : ").append(metrics.viewportRectLeft)
- .append(", \"y\" : ").append(metrics.viewportRectTop)
- .append(", \"zoom\" : ").append(metrics.zoomFactor)
- .append(", \"displayPort\" :").append(displayPort.toJSON())
- .append('}');
- event.mCharactersExtra = sb.toString();
- return event;
- }
-
- public static GeckoEvent createURILoadEvent(String uri) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOAD_URI);
- event.mCharacters = uri;
- event.mCharactersExtra = "";
- return event;
- }
-
- public static GeckoEvent createBookmarkLoadEvent(String uri) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOAD_URI);
- event.mCharacters = uri;
- event.mCharactersExtra = "-bookmark";
- return event;
- }
-
- public static GeckoEvent createVisitedEvent(String data) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.VISITED);
- event.mCharacters = data;
- return event;
- }
-
- public static GeckoEvent createNetworkEvent(int connectionType, boolean isWifi, int DHCPGateway, String status) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NETWORK_CHANGED);
- event.mConnectionType = connectionType;
- event.mIsWifi = isWifi;
- event.mDHCPGateway = DHCPGateway;
- event.mCharacters = status;
- return event;
- }
-
- public static GeckoEvent createThumbnailEvent(int tabId, int bufw, int bufh, ByteBuffer buffer) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.THUMBNAIL);
- event.mPoints = new Point[1];
- event.mPoints[0] = new Point(bufw, bufh);
- event.mMetaState = tabId;
- event.mBuffer = buffer;
- return event;
- }
-
- public static GeckoEvent createZoomedViewEvent(int tabId, int x, int y, int bufw, int bufh, float scaleFactor, ByteBuffer buffer) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.ZOOMEDVIEW);
- event.mPoints = new Point[2];
- event.mPoints[0] = new Point(x, y);
- event.mPoints[1] = new Point(bufw, bufh);
- event.mX = (double) scaleFactor;
- event.mMetaState = tabId;
- event.mBuffer = buffer;
- return event;
- }
-
- public static GeckoEvent createScreenOrientationEvent(short aScreenOrientation, short aScreenAngle) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.SCREENORIENTATION_CHANGED);
- event.mScreenOrientation = aScreenOrientation;
- event.mScreenAngle = aScreenAngle;
- return event;
- }
-
- public static GeckoEvent createCallObserverEvent(String observerKey, String topic, String data) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.CALL_OBSERVER);
- event.mCharacters = observerKey;
- event.mCharactersExtra = topic;
- event.mData = data;
- return event;
- }
-
- public static GeckoEvent createRemoveObserverEvent(String observerKey) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.REMOVE_OBSERVER);
- event.mCharacters = observerKey;
- return event;
- }
-
- public static GeckoEvent createLowMemoryEvent(int level) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOW_MEMORY);
- event.mMetaState = level;
- return event;
- }
-
- public static GeckoEvent createNetworkLinkChangeEvent(String status) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NETWORK_LINK_CHANGE);
- event.mCharacters = status;
- return event;
- }
-
- public static GeckoEvent createTelemetryHistogramAddEvent(String histogram,
- int value) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
- event.mCharacters = histogram;
- // Set the extras with null so that it cannot be mistaken with a keyed histogram.
- event.mCharactersExtra = null;
- event.mCount = value;
- return event;
- }
-
- public static GeckoEvent createTelemetryKeyedHistogramAddEvent(String histogram,
- String key,
- int value) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
- event.mCharacters = histogram;
- event.mCharactersExtra = key;
- event.mCount = value;
- return event;
- }
-
- public static GeckoEvent createTelemetryUISessionStartEvent(String session, long timestamp) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_SESSION_START);
- event.mCharacters = session;
- event.mTime = timestamp;
- return event;
- }
-
- public static GeckoEvent createTelemetryUISessionStopEvent(String session, String reason, long timestamp) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_SESSION_STOP);
- event.mCharacters = session;
- event.mCharactersExtra = reason;
- event.mTime = timestamp;
- return event;
- }
-
- public static GeckoEvent createTelemetryUIEvent(String action, String method, long timestamp, String extras) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_EVENT);
- event.mData = action;
- event.mCharacters = method;
- event.mCharactersExtra = extras;
- event.mTime = timestamp;
- return event;
- }
-
- public static GeckoEvent createGamepadAddRemoveEvent(int id, boolean added) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_ADDREMOVE);
- event.mID = id;
- event.mAction = added ? ACTION_GAMEPAD_ADDED : ACTION_GAMEPAD_REMOVED;
- return event;
- }
-
- private static int boolArrayToBitfield(boolean[] array) {
- int bits = 0;
- for (int i = 0; i < array.length; i++) {
- if (array[i]) {
- bits |= 1 << i;
- }
- }
- return bits;
- }
-
- public static GeckoEvent createGamepadButtonEvent(int id,
- int which,
- boolean pressed,
- float value) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
- event.mID = id;
- event.mAction = ACTION_GAMEPAD_BUTTON;
- event.mGamepadButton = which;
- event.mGamepadButtonPressed = pressed;
- event.mGamepadButtonValue = value;
- return event;
- }
-
- public static GeckoEvent createGamepadAxisEvent(int id, boolean[] valid,
- float[] values) {
- GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
- event.mID = id;
- event.mAction = ACTION_GAMEPAD_AXES;
- event.mFlags = boolArrayToBitfield(valid);
- event.mCount = values.length;
- event.mGamepadValues = values;
- return event;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoHalDefines.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* -*- 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;
-
-public class GeckoHalDefines
-{
- /*
- * Keep these values consistent with |SensorType| in HalSensor.h
- */
- public static final int SENSOR_ORIENTATION = 0;
- public static final int SENSOR_ACCELERATION = 1;
- public static final int SENSOR_PROXIMITY = 2;
- public static final int SENSOR_LINEAR_ACCELERATION = 3;
- public static final int SENSOR_GYROSCOPE = 4;
- public static final int SENSOR_LIGHT = 5;
- public static final int SENSOR_ROTATION_VECTOR = 6;
- public static final int SENSOR_GAME_ROTATION_VECTOR = 7;
-
- public static final int SENSOR_ACCURACY_UNKNOWN = -1;
- public static final int SENSOR_ACCURACY_UNRELIABLE = 0;
- public static final int SENSOR_ACCURACY_LOW = 1;
- public static final int SENSOR_ACCURACY_MED = 2;
- public static final int SENSOR_ACCURACY_HIGH = 3;
-};
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoInputConnection.java
+++ /dev/null
@@ -1,992 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-import java.util.concurrent.SynchronousQueue;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.util.Clipboard;
-import org.mozilla.gecko.util.GamepadUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.media.AudioManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.Selection;
-import android.text.SpannableString;
-import android.text.method.KeyListener;
-import android.text.method.TextKeyListener;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.inputmethod.BaseInputConnection;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputMethodManager;
-
-class GeckoInputConnection
- extends BaseInputConnection
- implements InputConnectionListener, GeckoEditableListener {
-
- private static final boolean DEBUG = false;
- protected static final String LOGTAG = "GeckoInputConnection";
-
- private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection";
- private static final String CUSTOM_HANDLER_TEST_CLASS =
- "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput";
-
- private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480;
-
- private static Handler sBackgroundHandler;
-
- // Managed only by notifyIMEContext; see comments in notifyIMEContext
- private int mIMEState;
- private String mIMETypeHint = "";
- private String mIMEModeHint = "";
- private String mIMEActionHint = "";
-
- private String mCurrentInputMethod = "";
-
- private final View mView;
- private final GeckoEditableClient mEditableClient;
- protected int mBatchEditCount;
- private ExtractedTextRequest mUpdateRequest;
- private final ExtractedText mUpdateExtract = new ExtractedText();
- private boolean mBatchSelectionChanged;
- private boolean mBatchTextChanged;
- private final InputConnection mKeyInputConnection;
-
- public static GeckoEditableListener create(View targetView,
- GeckoEditableClient editable) {
- if (DEBUG)
- return DebugGeckoInputConnection.create(targetView, editable);
- else
- return new GeckoInputConnection(targetView, editable);
- }
-
- protected GeckoInputConnection(View targetView,
- GeckoEditableClient editable) {
- super(targetView, true);
- mView = targetView;
- mEditableClient = editable;
- mIMEState = IME_STATE_DISABLED;
- // InputConnection that sends keys for plugins, which don't have full editors
- mKeyInputConnection = new BaseInputConnection(targetView, false);
- }
-
- @Override
- public synchronized boolean beginBatchEdit() {
- mBatchEditCount++;
- mEditableClient.setBatchMode(true);
- return true;
- }
-
- @Override
- public synchronized boolean endBatchEdit() {
- if (mBatchEditCount > 0) {
- mBatchEditCount--;
- if (mBatchEditCount == 0) {
- if (mBatchTextChanged) {
- notifyTextChange();
- mBatchTextChanged = false;
- }
- if (mBatchSelectionChanged) {
- Editable editable = getEditable();
- notifySelectionChange(Selection.getSelectionStart(editable),
- Selection.getSelectionEnd(editable));
- mBatchSelectionChanged = false;
- }
- mEditableClient.setBatchMode(false);
- }
- } else {
- Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!");
- }
- return true;
- }
-
- @Override
- public Editable getEditable() {
- return mEditableClient.getEditable();
- }
-
- @Override
- public boolean performContextMenuAction(int id) {
- Editable editable = getEditable();
- if (editable == null) {
- return false;
- }
- int selStart = Selection.getSelectionStart(editable);
- int selEnd = Selection.getSelectionEnd(editable);
-
- switch (id) {
- case android.R.id.selectAll:
- setSelection(0, editable.length());
- break;
- case android.R.id.cut:
- // If selection is empty, we'll select everything
- if (selStart == selEnd) {
- // Fill the clipboard
- Clipboard.setText(editable);
- editable.clear();
- } else {
- Clipboard.setText(
- editable.toString().substring(
- Math.min(selStart, selEnd),
- Math.max(selStart, selEnd)));
- editable.delete(selStart, selEnd);
- }
- break;
- case android.R.id.paste:
- commitText(Clipboard.getText(), 1);
- break;
- case android.R.id.copy:
- // Copy the current selection or the empty string if nothing is selected.
- String copiedText = selStart == selEnd ? "" :
- editable.toString().substring(
- Math.min(selStart, selEnd),
- Math.max(selStart, selEnd));
- Clipboard.setText(copiedText);
- break;
- }
- return true;
- }
-
- @Override
- public boolean performPrivateCommand(final String action, final Bundle data) {
- switch (action) {
- case "process-gecko-events":
- // Process all currently pending Gecko thread events before returning.
-
- final Editable editable = getEditable();
- if (editable == null) {
- return false;
- }
-
- // Removing an invalid span is essentially a no-op, but it does force the
- // current thread to wait for the Gecko thread when we call length(), in order
- // to process the removeSpan event. Once Gecko thread processes the removeSpan
- // event, all previous events in the Gecko event queue would have been
- // processed as well.
- editable.removeSpan(null);
- editable.length();
- return true;
- }
- return false;
- }
-
- @Override
- public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
- if (req == null)
- return null;
-
- if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0)
- mUpdateRequest = req;
-
- Editable editable = getEditable();
- if (editable == null) {
- return null;
- }
- int selStart = Selection.getSelectionStart(editable);
- int selEnd = Selection.getSelectionEnd(editable);
-
- ExtractedText extract = new ExtractedText();
- extract.flags = 0;
- extract.partialStartOffset = -1;
- extract.partialEndOffset = -1;
- extract.selectionStart = selStart;
- extract.selectionEnd = selEnd;
- extract.startOffset = 0;
- if ((req.flags & GET_TEXT_WITH_STYLES) != 0) {
- extract.text = new SpannableString(editable);
- } else {
- extract.text = editable.toString();
- }
- return extract;
- }
-
- private View getView() {
- return mView;
- }
-
- private InputMethodManager getInputMethodManager() {
- View view = getView();
- if (view == null) {
- return null;
- }
- Context context = view.getContext();
- return InputMethods.getInputMethodManager(context);
- }
-
- private void showSoftInput() {
- final View v = getView();
- final InputMethodManager imm = getInputMethodManager();
- if (v == null || imm == null) {
- return;
- }
-
- v.post(new Runnable() {
- @Override
- public void run() {
- if (v.hasFocus() && !imm.isActive(v)) {
- // Marshmallow workaround: The view has focus but it is not the active
- // view for the input method. (Bug 1211848)
- v.clearFocus();
- v.requestFocus();
- }
- imm.showSoftInput(v, 0);
- }
- });
- }
-
- private void hideSoftInput() {
- final InputMethodManager imm = getInputMethodManager();
- if (imm != null) {
- final View v = getView();
- imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
- }
- }
-
- private void restartInput() {
-
- final InputMethodManager imm = getInputMethodManager();
- if (imm == null) {
- return;
- }
- final View v = getView();
- // InputMethodManager has internal logic to detect if we are restarting input
- // in an already focused View, which is the case here because all content text
- // fields are inside one LayerView. When this happens, InputMethodManager will
- // tell the input method to soft reset instead of hard reset. Stock latin IME
- // on Android 4.2+ has a quirk that when it soft resets, it does not clear the
- // composition. The following workaround tricks the IME into clearing the
- // composition when soft resetting.
- if (InputMethods.needsSoftResetWorkaround(mCurrentInputMethod)) {
- // Fake a selection change, because the IME clears the composition when
- // the selection changes, even if soft-resetting. Offsets here must be
- // different from the previous selection offsets, and -1 seems to be a
- // reasonable, deterministic value
- notifySelectionChange(-1, -1);
- }
- try {
- imm.restartInput(v);
- } catch (RuntimeException e) {
- Log.e(LOGTAG, "Error restarting input", e);
- }
- }
-
- private void resetInputConnection() {
- if (mBatchEditCount != 0) {
- Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
- mBatchEditCount = 0;
- }
- mBatchSelectionChanged = false;
- mBatchTextChanged = false;
-
- // Do not reset mIMEState here; see comments in notifyIMEContext
- }
-
- @Override
- public void onTextChange(CharSequence text, int start, int oldEnd, int newEnd) {
-
- if (mUpdateRequest == null) {
- // Android always expects selection updates when not in extracted mode;
- // in extracted mode, the selection is reported through updateExtractedText
- final Editable editable = getEditable();
- if (editable != null) {
- onSelectionChange(Selection.getSelectionStart(editable),
- Selection.getSelectionEnd(editable));
- }
- return;
- }
-
- if (mBatchEditCount > 0) {
- // Delay notification until after the batch edit
- mBatchTextChanged = true;
- return;
- }
- notifyTextChange();
- }
-
- private void notifyTextChange() {
-
- final InputMethodManager imm = getInputMethodManager();
- final View v = getView();
- final Editable editable = getEditable();
- if (imm == null || v == null || editable == null) {
- return;
- }
- mUpdateExtract.flags = 0;
- // Update the entire Editable range
- mUpdateExtract.partialStartOffset = -1;
- mUpdateExtract.partialEndOffset = -1;
- mUpdateExtract.selectionStart =
- Selection.getSelectionStart(editable);
- mUpdateExtract.selectionEnd =
- Selection.getSelectionEnd(editable);
- mUpdateExtract.startOffset = 0;
- if ((mUpdateRequest.flags & GET_TEXT_WITH_STYLES) != 0) {
- mUpdateExtract.text = new SpannableString(editable);
- } else {
- mUpdateExtract.text = editable.toString();
- }
- imm.updateExtractedText(v, mUpdateRequest.token,
- mUpdateExtract);
- }
-
- @Override
- public void onSelectionChange(int start, int end) {
-
- if (mBatchEditCount > 0) {
- // Delay notification until after the batch edit
- mBatchSelectionChanged = true;
- return;
- }
-
- final Editable editable = getEditable();
- if (editable != null) {
- notifySelectionChange(Selection.getSelectionStart(editable),
- Selection.getSelectionEnd(editable));
- }
- }
-
- private void notifySelectionChange(int start, int end) {
-
- final InputMethodManager imm = getInputMethodManager();
- final View v = getView();
- final Editable editable = getEditable();
- if (imm == null || v == null || editable == null) {
- return;
- }
- imm.updateSelection(v, start, end, getComposingSpanStart(editable),
- getComposingSpanEnd(editable));
- }
-
- @Override
- public void onDefaultKeyEvent(final KeyEvent event) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- GeckoInputConnection.this.performDefaultKeyAction(event);
- }
- });
- }
-
- private static synchronized Handler getBackgroundHandler() {
- if (sBackgroundHandler != null) {
- return sBackgroundHandler;
- }
- // Don't use GeckoBackgroundThread because Gecko thread may block waiting on
- // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME,
- // GeckoBackgroundThread may end up also block waiting on Gecko thread and a
- // deadlock occurs
- Thread backgroundThread = new Thread(new Runnable() {
- @Override
- public void run() {
- Looper.prepare();
- synchronized (GeckoInputConnection.class) {
- sBackgroundHandler = new Handler();
- GeckoInputConnection.class.notify();
- }
- Looper.loop();
- // We should never be exiting the thread loop.
- throw new IllegalThreadStateException("unreachable code");
- }
- }, LOGTAG);
- backgroundThread.setDaemon(true);
- backgroundThread.start();
- while (sBackgroundHandler == null) {
- try {
- // wait for new thread to set sBackgroundHandler
- GeckoInputConnection.class.wait();
- } catch (InterruptedException e) {
- }
- }
- return sBackgroundHandler;
- }
-
- private boolean canReturnCustomHandler() {
- if (mIMEState == IME_STATE_DISABLED) {
- return false;
- }
- for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
- // We only return our custom Handler to InputMethodManager's InputConnection
- // proxy. For all other purposes, we return the regular Handler.
- // InputMethodManager retrieves the Handler for its InputConnection proxy
- // inside its method startInputInner(), so we check for that here. This is
- // valid from Android 2.2 to at least Android 4.2. If this situation ever
- // changes, we gracefully fall back to using the regular Handler.
- if ("startInputInner".equals(frame.getMethodName()) &&
- "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) {
- // only return our own Handler to InputMethodManager
- return true;
- }
- if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) &&
- CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) {
- // InputConnection tests should also run on the custom handler
- return true;
- }
- }
- return false;
- }
-
- private boolean isPhysicalKeyboardPresent() {
- final View v = getView();
- if (v == null) {
- return false;
- }
- final Configuration config = v.getContext().getResources().getConfiguration();
- return config.keyboard != Configuration.KEYBOARD_NOKEYS;
- }
-
- // Android N: @Override // InputConnection
- public Handler getHandler() {
- if (isPhysicalKeyboardPresent()) {
- return ThreadUtils.getUiHandler();
- }
-
- return getBackgroundHandler();
- }
-
- @Override // InputConnectionListener
- public Handler getHandler(Handler defHandler) {
- if (!canReturnCustomHandler()) {
- return defHandler;
- }
-
- return mEditableClient.setInputConnectionHandler(getHandler());
- }
-
- @Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- // Some keyboards require us to fill out outAttrs even if we return null.
- outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
- outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
- outAttrs.actionLabel = null;
-
- if (mIMEState == IME_STATE_DISABLED) {
- hideSoftInput();
- return null;
- }
-
- if (mIMEState == IME_STATE_PASSWORD ||
- "password".equalsIgnoreCase(mIMETypeHint))
- outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
- else if (mIMEState == IME_STATE_PLUGIN)
- outAttrs.inputType = InputType.TYPE_NULL; // "send key events" mode
- else if (mIMETypeHint.equalsIgnoreCase("url"))
- outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
- else if (mIMETypeHint.equalsIgnoreCase("email"))
- outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
- else if (mIMETypeHint.equalsIgnoreCase("tel"))
- outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
- else if (mIMETypeHint.equalsIgnoreCase("number") ||
- mIMETypeHint.equalsIgnoreCase("range"))
- outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
- | InputType.TYPE_NUMBER_FLAG_SIGNED
- | InputType.TYPE_NUMBER_FLAG_DECIMAL;
- else if (mIMETypeHint.equalsIgnoreCase("week") ||
- mIMETypeHint.equalsIgnoreCase("month"))
- outAttrs.inputType = InputType.TYPE_CLASS_DATETIME
- | InputType.TYPE_DATETIME_VARIATION_DATE;
- else if (mIMEModeHint.equalsIgnoreCase("numeric"))
- outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
- InputType.TYPE_NUMBER_FLAG_SIGNED |
- InputType.TYPE_NUMBER_FLAG_DECIMAL;
- else if (mIMEModeHint.equalsIgnoreCase("digit"))
- outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
- else {
- // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap
- outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
- InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
- if (mIMETypeHint.equalsIgnoreCase("textarea") ||
- mIMETypeHint.length() == 0) {
- // empty mIMETypeHint indicates contentEditable/designMode documents
- outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
- }
- if (mIMEModeHint.equalsIgnoreCase("uppercase"))
- outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
- else if (mIMEModeHint.equalsIgnoreCase("titlecase"))
- outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
- else if (mIMETypeHint.equalsIgnoreCase("text") &&
- !mIMEModeHint.equalsIgnoreCase("autocapitalized"))
- outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_NORMAL;
- else if (!mIMEModeHint.equalsIgnoreCase("lowercase"))
- outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
- // auto-capitalized mode is the default for types other than text
- }
-
- if (mIMEActionHint.equalsIgnoreCase("go"))
- outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
- else if (mIMEActionHint.equalsIgnoreCase("done"))
- outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
- else if (mIMEActionHint.equalsIgnoreCase("next"))
- outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
- else if (mIMEActionHint.equalsIgnoreCase("search") ||
- mIMETypeHint.equalsIgnoreCase("search"))
- outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
- else if (mIMEActionHint.equalsIgnoreCase("send"))
- outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
- else if (mIMEActionHint.length() > 0) {
- if (DEBUG)
- Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\"");
- outAttrs.actionLabel = mIMEActionHint;
- }
-
- Context context = getView().getContext();
- DisplayMetrics metrics = context.getResources().getDisplayMetrics();
- if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) {
- // prevent showing full-screen keyboard only when the screen is tall enough
- // to show some reasonable amount of the page (see bug 752709)
- outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI
- | EditorInfo.IME_FLAG_NO_FULLSCREEN;
- }
-
- if (DEBUG) {
- Log.d(LOGTAG, "mapped IME states to: inputType = " +
- Integer.toHexString(outAttrs.inputType) + ", imeOptions = " +
- Integer.toHexString(outAttrs.imeOptions));
- }
-
- String prevInputMethod = mCurrentInputMethod;
- mCurrentInputMethod = InputMethods.getCurrentInputMethod(context);
- if (DEBUG) {
- Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod);
- }
-
- // If the user has changed IMEs, then notify input method observers.
- if (!mCurrentInputMethod.equals(prevInputMethod) && GeckoAppShell.getGeckoInterface() != null) {
- GeckoAppShell.getGeckoInterface().onInputMethodChanged(mCurrentInputMethod);
- }
-
- if (mIMEState == IME_STATE_PLUGIN) {
- // Since we are using a temporary string as the editable, the selection is at 0
- outAttrs.initialSelStart = 0;
- outAttrs.initialSelEnd = 0;
- return mKeyInputConnection;
- }
- Editable editable = getEditable();
- outAttrs.initialSelStart = Selection.getSelectionStart(editable);
- outAttrs.initialSelEnd = Selection.getSelectionEnd(editable);
-
- showSoftInput();
- return this;
- }
-
- private boolean replaceComposingSpanWithSelection() {
- final Editable content = getEditable();
- if (content == null) {
- return false;
- }
- int a = getComposingSpanStart(content),
- b = getComposingSpanEnd(content);
- if (a != -1 && b != -1) {
- if (DEBUG) {
- Log.d(LOGTAG, "removing composition at " + a + "-" + b);
- }
- removeComposingSpans(content);
- Selection.setSelection(content, a, b);
- }
- return true;
- }
-
- @Override
- public boolean commitText(CharSequence text, int newCursorPosition) {
- if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod) &&
- text.length() == 1 && newCursorPosition > 0) {
- if (DEBUG) {
- Log.d(LOGTAG, "committing \"" + text + "\" as key");
- }
- // mKeyInputConnection is a BaseInputConnection that commits text as keys;
- // but we first need to replace any composing span with a selection,
- // so that the new key events will generate characters to replace
- // text from the old composing span
- return replaceComposingSpanWithSelection() &&
- mKeyInputConnection.commitText(text, newCursorPosition);
- }
- return super.commitText(text, newCursorPosition);
- }
-
- @Override
- public boolean setSelection(int start, int end) {
- if (start < 0 || end < 0) {
- // Some keyboards (e.g. Samsung) can call setSelection with
- // negative offsets. In that case we ignore the call, similar to how
- // BaseInputConnection.setSelection ignores offsets that go past the length.
- return true;
- }
- return super.setSelection(start, end);
- }
-
- /* package */ void sendKeyEvent(final int action, KeyEvent event) {
- final Editable editable = getEditable();
- if (editable == null) {
- return;
- }
-
- final KeyListener keyListener = TextKeyListener.getInstance();
- event = translateKey(event.getKeyCode(), event);
-
- // We only let TextKeyListener do UI things on the UI thread.
- final View v = ThreadUtils.isOnUiThread() ? getView() : null;
- final int keyCode = event.getKeyCode();
- final boolean handled;
-
- if (shouldSkipKeyListener(keyCode, event)) {
- handled = false;
- } else if (action == KeyEvent.ACTION_DOWN) {
- mEditableClient.setSuppressKeyUp(true);
- handled = keyListener.onKeyDown(v, editable, keyCode, event);
- } else if (action == KeyEvent.ACTION_UP) {
- handled = keyListener.onKeyUp(v, editable, keyCode, event);
- } else {
- handled = keyListener.onKeyOther(v, editable, event);
- }
-
- if (!handled) {
- mEditableClient.sendKeyEvent(event, action, TextKeyListener.getMetaState(editable));
- }
-
- if (action == KeyEvent.ACTION_DOWN) {
- if (!handled) {
- // Usually, the down key listener call above adjusts meta states for us.
- // However, if the call didn't handle the event, we have to manually
- // adjust meta states so the meta states remain consistent.
- TextKeyListener.adjustMetaAfterKeypress(editable);
- }
- mEditableClient.setSuppressKeyUp(false);
- }
- }
-
- @Override
- public boolean sendKeyEvent(KeyEvent event) {
- sendKeyEvent(event.getAction(), event);
- return false; // seems to always return false
- }
-
- @Override
- public boolean onKeyPreIme(int keyCode, KeyEvent event) {
- return false;
- }
-
- private boolean shouldProcessKey(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_MENU:
- case KeyEvent.KEYCODE_BACK:
- case KeyEvent.KEYCODE_VOLUME_UP:
- case KeyEvent.KEYCODE_VOLUME_DOWN:
- case KeyEvent.KEYCODE_SEARCH:
- // ignore HEADSETHOOK to allow hold-for-voice-search to work
- case KeyEvent.KEYCODE_HEADSETHOOK:
- return false;
- }
- return true;
- }
-
- private boolean shouldSkipKeyListener(int keyCode, KeyEvent event) {
- if (mIMEState == IME_STATE_DISABLED ||
- mIMEState == IME_STATE_PLUGIN) {
- return true;
- }
- // Preserve enter and tab keys for the browser
- if (keyCode == KeyEvent.KEYCODE_ENTER ||
- keyCode == KeyEvent.KEYCODE_TAB) {
- return true;
- }
- // BaseKeyListener returns false even if it handled these keys for us,
- // so we skip the key listener entirely and handle these ourselves
- if (keyCode == KeyEvent.KEYCODE_DEL ||
- keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
- return true;
- }
- return false;
- }
-
- private KeyEvent translateKey(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_ENTER:
- if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 &&
- mIMEActionHint.equalsIgnoreCase("next")) {
- return new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB);
- }
- break;
- }
-
- if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) {
- return GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event);
- }
-
- return event;
- }
-
- // Called by OnDefaultKeyEvent handler, up from Gecko
- /* package */ void performDefaultKeyAction(KeyEvent event) {
- switch (event.getKeyCode()) {
- case KeyEvent.KEYCODE_MUTE:
- case KeyEvent.KEYCODE_HEADSETHOOK:
- case KeyEvent.KEYCODE_MEDIA_PLAY:
- case KeyEvent.KEYCODE_MEDIA_PAUSE:
- case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
- case KeyEvent.KEYCODE_MEDIA_STOP:
- case KeyEvent.KEYCODE_MEDIA_NEXT:
- case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
- case KeyEvent.KEYCODE_MEDIA_REWIND:
- case KeyEvent.KEYCODE_MEDIA_RECORD:
- case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
- case KeyEvent.KEYCODE_MEDIA_CLOSE:
- case KeyEvent.KEYCODE_MEDIA_EJECT:
- case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
- // Forward media keypresses to the registered handler so headset controls work
- // Does the same thing as Chromium
- // https://chromium.googlesource.com/chromium/src/+/49.0.2623.67/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsDelegateAndroid.java#445
- // These are all the keys dispatchMediaKeyEvent supports.
- if (AppConstants.Versions.feature19Plus) {
- // dispatchMediaKeyEvent is only available on Android 4.4+
- Context viewContext = getView().getContext();
- AudioManager am = (AudioManager)viewContext.getSystemService(Context.AUDIO_SERVICE);
- am.dispatchMediaKeyEvent(event);
- }
- break;
- }
- }
-
- private boolean processKey(final int action, final int keyCode, final KeyEvent event) {
-
- if (keyCode > KeyEvent.getMaxKeyCode() || !shouldProcessKey(keyCode, event)) {
- return false;
- }
-
- mEditableClient.postToInputConnection(new Runnable() {
- @Override
- public void run() {
- sendKeyEvent(action, event);
- }
- });
- return true;
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- return processKey(KeyEvent.ACTION_DOWN, keyCode, event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- return processKey(KeyEvent.ACTION_UP, keyCode, event);
- }
-
- /**
- * Get a key that represents a given character.
- */
- private KeyEvent getCharKeyEvent(final char c) {
- final long time = SystemClock.uptimeMillis();
- return new KeyEvent(time, time, KeyEvent.ACTION_MULTIPLE,
- KeyEvent.KEYCODE_UNKNOWN, /* repeat */ 0) {
- @Override
- public int getUnicodeChar() {
- return c;
- }
-
- @Override
- public int getUnicodeChar(int metaState) {
- return c;
- }
- };
- }
-
- @Override
- public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
- // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters()
- final String str = event.getCharacters();
- for (int i = 0; i < str.length(); i++) {
- final KeyEvent charEvent = getCharKeyEvent(str.charAt(i));
- if (!processKey(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN, charEvent) ||
- !processKey(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_UNKNOWN, charEvent)) {
- return false;
- }
- }
- return true;
- }
-
- while ((repeatCount--) != 0) {
- if (!processKey(KeyEvent.ACTION_DOWN, keyCode, event) ||
- !processKey(KeyEvent.ACTION_UP, keyCode, event)) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public boolean onKeyLongPress(int keyCode, KeyEvent event) {
- View v = getView();
- switch (keyCode) {
- case KeyEvent.KEYCODE_MENU:
- InputMethodManager imm = getInputMethodManager();
- imm.toggleSoftInputFromWindow(v.getWindowToken(),
- InputMethodManager.SHOW_FORCED, 0);
- return true;
- default:
- break;
- }
- return false;
- }
-
- @Override
- public boolean isIMEEnabled() {
- // make sure this picks up PASSWORD and PLUGIN states as well
- return mIMEState != IME_STATE_DISABLED;
- }
-
- @Override
- public void notifyIME(int type) {
- switch (type) {
-
- case NOTIFY_IME_OF_FOCUS:
- case NOTIFY_IME_OF_BLUR:
- // Showing/hiding vkb is done in notifyIMEContext
- resetInputConnection();
- break;
-
- case NOTIFY_IME_OPEN_VKB:
- showSoftInput();
- break;
-
- default:
- if (DEBUG) {
- throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
- }
- break;
- }
- }
-
- @Override
- public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint) {
- // For some input type we will use a widget to display the ui, for those we must not
- // display the ime. We can display a widget for date and time types and, if the sdk version
- // is 11 or greater, for datetime/month/week as well.
- if (typeHint != null &&
- (typeHint.equalsIgnoreCase("date") ||
- typeHint.equalsIgnoreCase("time") ||
- (Versions.feature11Plus && (typeHint.equalsIgnoreCase("datetime") ||
- typeHint.equalsIgnoreCase("month") ||
- typeHint.equalsIgnoreCase("week") ||
- typeHint.equalsIgnoreCase("datetime-local"))))) {
- state = IME_STATE_DISABLED;
- }
-
- // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext,
- // and not reset anywhere else. Usually, notifyIMEContext is called right after a
- // focus or blur, so resetting mIMEState during the focus or blur seems harmless.
- // However, this behavior is not guaranteed. Gecko may call notifyIMEContext
- // independent of focus change; that is, a focus change may not be accompanied by
- // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not
- // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318)
- /* When IME is 'disabled', IME processing is disabled.
- In addition, the IME UI is hidden */
- mIMEState = state;
- mIMETypeHint = (typeHint == null) ? "" : typeHint;
- mIMEModeHint = (modeHint == null) ? "" : modeHint;
- mIMEActionHint = (actionHint == null) ? "" : actionHint;
-
- // These fields are reset here and will be updated when restartInput is called below
- mUpdateRequest = null;
- mCurrentInputMethod = "";
-
- View v = getView();
- if (v == null || !v.hasFocus()) {
- // When using Find In Page, we can still receive notifyIMEContext calls due to the
- // selection changing when highlighting. However in this case we don't want to reset/
- // show/hide the keyboard because the find box has the focus and is taking input from
- // the keyboard.
- return;
- }
- restartInput();
- }
-}
-
-final class DebugGeckoInputConnection
- extends GeckoInputConnection
- implements InvocationHandler {
-
- private InputConnection mProxy;
- private final StringBuilder mCallLevel;
-
- private DebugGeckoInputConnection(View targetView,
- GeckoEditableClient editable) {
- super(targetView, editable);
- mCallLevel = new StringBuilder();
- }
-
- public static GeckoEditableListener create(View targetView,
- GeckoEditableClient editable) {
- final Class<?>[] PROXY_INTERFACES = { InputConnection.class,
- InputConnectionListener.class,
- GeckoEditableListener.class };
- DebugGeckoInputConnection dgic =
- new DebugGeckoInputConnection(targetView, editable);
- dgic.mProxy = (InputConnection)Proxy.newProxyInstance(
- GeckoInputConnection.class.getClassLoader(),
- PROXY_INTERFACES, dgic);
- return (GeckoEditableListener)dgic.mProxy;
- }
-
- @Override
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
-
- StringBuilder log = new StringBuilder(mCallLevel);
- log.append("> ").append(method.getName()).append("(");
- if (args != null) {
- for (Object arg : args) {
- // translate argument values to constant names
- if ("notifyIME".equals(method.getName()) && arg == args[0]) {
- log.append(GeckoEditable.getConstantName(
- GeckoEditableListener.class, "NOTIFY_IME_", arg));
- } else if ("notifyIMEContext".equals(method.getName()) && arg == args[0]) {
- log.append(GeckoEditable.getConstantName(
- GeckoEditableListener.class, "IME_STATE_", arg));
- } else {
- GeckoEditable.debugAppend(log, arg);
- }
- log.append(", ");
- }
- if (args.length > 0) {
- log.setLength(log.length() - 2);
- }
- }
- log.append(")");
- Log.d(LOGTAG, log.toString());
-
- mCallLevel.append(' ');
- Object ret = method.invoke(this, args);
- if (ret == this) {
- ret = mProxy;
- }
- mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1));
-
- log.setLength(mCallLevel.length());
- log.append("< ").append(method.getName());
- if (!method.getReturnType().equals(Void.TYPE)) {
- GeckoEditable.debugAppend(log.append(": "), ret);
- }
- Log.d(LOGTAG, log.toString());
- return ret;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/* -*- 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(allowMultithread = true, stubName = "GetThreadNameJavaProfilingWrapper")
- 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(allowMultithread = true, stubName = "GetSampleTimeJavaProfiling")
- 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(allowMultithread = true, stubName = "GetFrameNameJavaProfilingWrapper")
- 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(allowMultithread = true, stubName = "StartJavaProfiling")
- 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(allowMultithread = true, stubName = "PauseJavaProfiling")
- public static void pause() {
- synchronized (GeckoJavaSampler.class) {
- sSamplingRunnable.mPauseSampler = true;
- }
- }
-
- @WrapForJNI(allowMultithread = true, stubName = "UnpauseJavaProfiling")
- public static void unpause() {
- synchronized (GeckoJavaSampler.class) {
- sSamplingRunnable.mPauseSampler = false;
- }
- }
-
- @WrapForJNI(allowMultithread = true, stubName = "StopJavaProfiling")
- 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;
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoNetworkManager.java
+++ /dev/null
@@ -1,436 +0,0 @@
-/* -*- 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.JNITarget;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NetworkUtils;
-import org.mozilla.gecko.util.NetworkUtils.ConnectionSubType;
-import org.mozilla.gecko.util.NetworkUtils.ConnectionType;
-import org.mozilla.gecko.util.NetworkUtils.NetworkStatus;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.DhcpInfo;
-import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiManager;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.telephony.TelephonyManager;
-import android.text.format.Formatter;
-import android.util.Log;
-
-/**
- * Provides connection type, subtype and general network status (up/down).
- *
- * According to spec of Network Information API version 3, connection types include:
- * bluetooth, cellular, ethernet, none, wifi and other. The objective of providing such general
- * connection is due to some security concerns. In short, we don't want to expose exact network type,
- * especially the cellular network type.
- *
- * Specific mobile subtypes are mapped to general 2G, 3G and 4G buckets.
- *
- * Logic is implemented as a state machine, so see the transition matrix to figure out what happens when.
- * This class depends on access to the context, so only use after GeckoAppShell has been initialized.
- */
-public class GeckoNetworkManager extends BroadcastReceiver implements NativeEventListener {
- private static final String LOGTAG = "GeckoNetworkManager";
-
- private static final String LINK_DATA_CHANGED = "changed";
-
- private static GeckoNetworkManager instance;
-
- public static void destroy() {
- if (instance != null) {
- instance.onDestroy();
- instance = null;
- }
- }
-
- public enum ManagerState {
- OffNoListeners,
- OffWithListeners,
- OnNoListeners,
- OnWithListeners
- }
-
- public enum ManagerEvent {
- start,
- stop,
- enableNotifications,
- disableNotifications,
- receivedUpdate
- }
-
- private ManagerState currentState = ManagerState.OffNoListeners;
- private ConnectionType currentConnectionType = ConnectionType.NONE;
- private NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
- private ConnectionSubType currentConnectionSubtype = ConnectionSubType.UNKNOWN;
-
- private enum InfoType {
- MCC,
- MNC
- }
-
- private GeckoNetworkManager() {
- EventDispatcher.getInstance().registerGeckoThreadListener(this,
- "Wifi:Enable",
- "Wifi:GetIPAddress");
- }
-
- private void onDestroy() {
- handleManagerEvent(ManagerEvent.stop);
- EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
- "Wifi:Enable",
- "Wifi:GetIPAddress");
- }
-
- public static GeckoNetworkManager getInstance() {
- if (instance == null) {
- instance = new GeckoNetworkManager();
- }
-
- return instance;
- }
-
- public double[] getCurrentInformation() {
- final Context applicationContext = GeckoAppShell.getApplicationContext();
- final ConnectionType connectionType = currentConnectionType;
- return new double[] {
- connectionType.value,
- connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
- connectionType == ConnectionType.WIFI ? wifiDhcpGatewayAddress(applicationContext) : 0.0
- };
- }
-
- @Override
- public void onReceive(Context aContext, Intent aIntent) {
- handleManagerEvent(ManagerEvent.receivedUpdate);
- }
-
- public void start() {
- handleManagerEvent(ManagerEvent.start);
- }
-
- public void stop() {
- handleManagerEvent(ManagerEvent.stop);
- }
-
- public void enableNotifications() {
- handleManagerEvent(ManagerEvent.enableNotifications);
- }
-
- public void disableNotifications() {
- handleManagerEvent(ManagerEvent.disableNotifications);
- }
-
- /**
- * For a given event, figure out the next state, run any transition by-product actions, and switch
- * current state to the next state. If event is invalid for the current state, this is a no-op.
- *
- * @param event Incoming event
- * @return Boolean indicating if transition was performed.
- */
- private synchronized boolean handleManagerEvent(ManagerEvent event) {
- final ManagerState nextState = getNextState(currentState, event);
-
- Log.d(LOGTAG, "Incoming event " + event + " for state " + currentState + " -> " + nextState);
- if (nextState == null) {
- Log.w(LOGTAG, "Invalid event " + event + " for state " + currentState);
- return false;
- }
-
- performActionsForStateEvent(currentState, event);
- currentState = nextState;
-
- return true;
- }
-
- /**
- * Defines a transition matrix for our state machine. For a given state/event pair, returns nextState.
- *
- * @param currentState Current state against which we have an incoming event
- * @param event Incoming event for which we'd like to figure out the next state
- * @return State into which we should transition as result of given event
- */
- @Nullable
- public static ManagerState getNextState(@NonNull ManagerState currentState, @NonNull ManagerEvent event) {
- switch (currentState) {
- case OffNoListeners:
- switch (event) {
- case start:
- return ManagerState.OnNoListeners;
- case enableNotifications:
- return ManagerState.OffWithListeners;
- default:
- return null;
- }
- case OnNoListeners:
- switch (event) {
- case stop:
- return ManagerState.OffNoListeners;
- case enableNotifications:
- return ManagerState.OnWithListeners;
- case receivedUpdate:
- return ManagerState.OnNoListeners;
- default:
- return null;
- }
- case OnWithListeners:
- switch (event) {
- case stop:
- return ManagerState.OffWithListeners;
- case disableNotifications:
- return ManagerState.OnNoListeners;
- case receivedUpdate:
- return ManagerState.OnWithListeners;
- default:
- return null;
- }
- case OffWithListeners:
- switch (event) {
- case start:
- return ManagerState.OnWithListeners;
- case disableNotifications:
- return ManagerState.OffNoListeners;
- default:
- return null;
- }
- default:
- throw new IllegalStateException("Unknown current state: " + currentState.name());
- }
- }
-
- /**
- * For a given state/event combination, run any actions which are by-products of leaving the state
- * because of a given event. Since this is a deterministic state machine, we can easily do that
- * without any additional information.
- *
- * @param currentState State which we are leaving
- * @param event Event which is causing us to leave the state
- */
- private void performActionsForStateEvent(ManagerState currentState, ManagerEvent event) {
- // NB: network state might be queried via getCurrentInformation at any time; pre-rewrite behaviour was
- // that network state was updated whenever enableNotifications was called. To avoid deviating
- // from previous behaviour and causing weird side-effects, we call updateNetworkStateAndConnectionType
- // whenever notifications are enabled.
- switch (currentState) {
- case OffNoListeners:
- if (event == ManagerEvent.start) {
- updateNetworkStateAndConnectionType();
- registerBroadcastReceiver();
- }
- if (event == ManagerEvent.enableNotifications) {
- updateNetworkStateAndConnectionType();
- }
- break;
- case OnNoListeners:
- if (event == ManagerEvent.receivedUpdate) {
- updateNetworkStateAndConnectionType();
- sendNetworkStateToListeners();
- }
- if (event == ManagerEvent.enableNotifications) {
- updateNetworkStateAndConnectionType();
- registerBroadcastReceiver();
- }
- if (event == ManagerEvent.stop) {
- unregisterBroadcastReceiver();
- }
- break;
- case OnWithListeners:
- if (event == ManagerEvent.receivedUpdate) {
- updateNetworkStateAndConnectionType();
- sendNetworkStateToListeners();
- }
- if (event == ManagerEvent.stop) {
- unregisterBroadcastReceiver();
- }
- /* no-op event: ManagerEvent.disableNotifications */
- break;
- case OffWithListeners:
- if (event == ManagerEvent.start) {
- registerBroadcastReceiver();
- }
- /* no-op event: ManagerEvent.disableNotifications */
- break;
- default:
- throw new IllegalStateException("Unknown current state: " + currentState.name());
- }
- }
-
- /**
- * Update current network state and connection types.
- */
- private void updateNetworkStateAndConnectionType() {
- final Context applicationContext = GeckoAppShell.getApplicationContext();
- final ConnectivityManager connectivityManager = (ConnectivityManager) applicationContext.getSystemService(
- Context.CONNECTIVITY_SERVICE);
- // Type/status getters below all have a defined behaviour for when connectivityManager == null
- if (connectivityManager == null) {
- Log.e(LOGTAG, "ConnectivityManager does not exist.");
- }
- currentConnectionType = NetworkUtils.getConnectionType(connectivityManager);
- currentNetworkStatus = NetworkUtils.getNetworkStatus(connectivityManager);
- currentConnectionSubtype = NetworkUtils.getConnectionSubType(connectivityManager);
- Log.d(LOGTAG, "New network state: " + currentNetworkStatus + ", " + currentConnectionType + ", " + currentConnectionSubtype);
- }
-
- /**
- * Send current network state and connection type as a GeckoEvent, to whomever is listening.
- */
- private void sendNetworkStateToListeners() {
- if (GeckoThread.isRunning()) {
- final Context applicationContext = GeckoAppShell.getApplicationContext();
- GeckoAppShell.sendEventToGecko(
- GeckoEvent.createNetworkEvent(
- currentConnectionType.value,
- currentConnectionType == ConnectionType.WIFI,
- wifiDhcpGatewayAddress(applicationContext),
- currentConnectionSubtype.value
- )
- );
-
- GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(currentNetworkStatus.value));
- GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(LINK_DATA_CHANGED));
- }
- }
-
- /**
- * Stop listening for network state updates.
- */
- private void unregisterBroadcastReceiver() {
- GeckoAppShell.getApplicationContext().unregisterReceiver(this);
- }
-
- /**
- * Start listening for network state updates.
- */
- private void registerBroadcastReceiver() {
- final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
- if (GeckoAppShell.getApplicationContext().registerReceiver(this, filter) == null) {
- Log.e(LOGTAG, "Registering receiver failed");
- }
- }
-
- private static int wifiDhcpGatewayAddress(Context context) {
- if (context == null) {
- return 0;
- }
-
- try {
- WifiManager mgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
- DhcpInfo d = mgr.getDhcpInfo();
- if (d == null) {
- return 0;
- }
-
- return d.gateway;
-
- } catch (Exception ex) {
- // getDhcpInfo() is not documented to require any permissions, but on some devices
- // requires android.permission.ACCESS_WIFI_STATE. Just catch the generic exception
- // here and returning 0. Not logging because this could be noisy.
- return 0;
- }
- }
-
- @Override
- /**
- * Handles native messages, not part of the state machine flow.
- */
- public void handleMessage(final String event, final NativeJSObject message,
- final EventCallback callback) {
- final Context applicationContext = GeckoAppShell.getApplicationContext();
- switch (event) {
- case "Wifi:Enable":
- final WifiManager mgr = (WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE);
-
- if (!mgr.isWifiEnabled()) {
- mgr.setWifiEnabled(true);
- } else {
- // If Wifi is enabled, maybe you need to select a network
- Intent intent = new Intent(android.provider.Settings.ACTION_WIFI_SETTINGS);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- applicationContext.startActivity(intent);
- }
- break;
- case "Wifi:GetIPAddress":
- getWifiIPAddress(callback);
- break;
- }
- }
-
- // This function only works for IPv4
- private void getWifiIPAddress(final EventCallback callback) {
- final WifiManager mgr = (WifiManager) GeckoAppShell.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
-
- if (mgr == null) {
- callback.sendError("Cannot get WifiManager");
- return;
- }
-
- final WifiInfo info = mgr.getConnectionInfo();
- if (info == null) {
- callback.sendError("Cannot get connection info");
- return;
- }
-
- int ip = info.getIpAddress();
- if (ip == 0) {
- callback.sendError("Cannot get IPv4 address");
- return;
- }
- callback.sendSuccess(Formatter.formatIpAddress(ip));
- }
-
- private static int getNetworkOperator(InfoType type, Context context) {
- if (null == context) {
- return -1;
- }
-
- TelephonyManager tel = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- if (tel == null) {
- Log.e(LOGTAG, "Telephony service does not exist");
- return -1;
- }
-
- String networkOperator = tel.getNetworkOperator();
- if (networkOperator == null || networkOperator.length() <= 3) {
- return -1;
- }
-
- if (type == InfoType.MNC) {
- return Integer.parseInt(networkOperator.substring(3));
- }
-
- if (type == InfoType.MCC) {
- return Integer.parseInt(networkOperator.substring(0, 3));
- }
-
- return -1;
- }
-
- /**
- * These are called from JavaScript ctypes. Avoid letting ProGuard delete them.
- *
- * Note that these methods must only be called after GeckoAppShell has been
- * initialized: they depend on access to the context.
- */
- @JNITarget
- public static int getMCC() {
- return getNetworkOperator(InfoType.MCC, GeckoAppShell.getApplicationContext());
- }
-
- @JNITarget
- public static int getMNC() {
- return getNetworkOperator(InfoType.MNC, GeckoAppShell.getApplicationContext());
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ /dev/null
@@ -1,943 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.support.annotation.WorkerThread;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
-import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.util.FileUtils;
-import org.mozilla.gecko.util.INIParser;
-import org.mozilla.gecko.util.INISection;
-import org.mozilla.gecko.util.IntentUtils;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.nio.charset.Charset;
-import java.util.Enumeration;
-import java.util.Hashtable;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public final class GeckoProfile {
- private static final String LOGTAG = "GeckoProfile";
-
- // The path in the profile to the file containing the client ID.
- private static final String CLIENT_ID_FILE_PATH = "datareporting/state.json";
- private static final String FHR_CLIENT_ID_FILE_PATH = "healthreport/state.json";
- // In the client ID file, the attribute title in the JSON object containing the client ID value.
- private static final String CLIENT_ID_JSON_ATTR = "clientID";
-
- private static final String TIMES_PATH = "times.json";
- private static final String PROFILE_CREATION_DATE_JSON_ATTR = "created";
-
- // Only tests should need to do this.
- // We can default this to AppConstants.RELEASE_BUILD once we fix Bug 1069687.
- private static volatile boolean sAcceptDirectoryChanges = true;
-
- @RobocopTarget
- public static void enableDirectoryChanges() {
- Log.w(LOGTAG, "Directory changes should only be enabled for tests. And even then it's a bad idea.");
- sAcceptDirectoryChanges = true;
- }
-
- public static final String DEFAULT_PROFILE = "default";
- // Profile is using a custom directory outside of the Mozilla directory.
- public static final String CUSTOM_PROFILE = "";
- public static final String GUEST_PROFILE_DIR = "guest";
- public static final String GUEST_MODE_PREF = "guestMode";
-
- public static final String PREF_FIRSTRUN_ENABLED = "startpane_enabled";
-
- // Session store
- private static final String SESSION_FILE = "sessionstore.js";
- private static final String SESSION_FILE_BACKUP = "sessionstore.bak";
- private static final long MAX_BACKUP_FILE_AGE = 1000 * 3600 * 24; // 24 hours
-
- private boolean mOldSessionDataProcessed = false;
-
- private static final ConcurrentHashMap<String, GeckoProfile> sProfileCache =
- new ConcurrentHashMap<String, GeckoProfile>(
- /* capacity */ 4, /* load factor */ 0.75f, /* concurrency */ 2);
- private static String sDefaultProfileName;
-
- private final String mName;
- private final File mMozillaDir;
- private final Context mApplicationContext;
-
- /**
- * Access to this member should be synchronized to avoid
- * races during creation -- particularly between getDir and GeckoView#init.
- *
- * Not final because this is lazily computed.
- */
- private File mProfileDir;
-
- private Boolean mInGuestMode;
-
- public static GeckoProfile initFromArgs(final Context context, final String args) {
- if (shouldUse(context)) {
- final GeckoProfile guestProfile = getGuestProfile(context);
- if (guestProfile != null) {
- return guestProfile;
- }
- // Failed to create guest profile; leave guest mode.
- leave(context);
- }
-
- // We never want to use the guest mode profile concurrently with a normal profile
- // -- no syncing to it, no dual-profile usage, nothing. GeckoThread startup with
- // a conventional GeckoProfile will cause the guest profile to be deleted and
- // guest mode to reset.
- if (getGuestDir(context).isDirectory()) {
- final GeckoProfile guestProfile = getGuestProfile(context);
- if (guestProfile != null) {
- removeProfile(context, guestProfile);
- }
- }
-
- String profileName = null;
- String profilePath = null;
-
- if (args != null && args.contains("-P")) {
- final Pattern p = Pattern.compile("(?:-P\\s*)(\\w*)(\\s*)");
- final Matcher m = p.matcher(args);
- if (m.find()) {
- profileName = m.group(1);
- }
- }
-
- if (args != null && args.contains("-profile")) {
- final Pattern p = Pattern.compile("(?:-profile\\s*)(\\S*)(\\s*)");
- final Matcher m = p.matcher(args);
- if (m.find()) {
- profilePath = m.group(1);
- }
- }
-
- if (profileName == null && profilePath == null) {
- // Get the default profile for the Activity.
- return getDefaultProfile(context);
- }
-
- return GeckoProfile.get(context, profileName, profilePath);
- }
-
- private static GeckoProfile getDefaultProfile(Context context) {
- try {
- return get(context, getDefaultProfileName(context));
-
- } catch (final NoMozillaDirectoryException e) {
- // If this failed, we're screwed.
- Log.wtf(LOGTAG, "Unable to get default profile name.", e);
- throw new RuntimeException(e);
- }
- }
-
- public static GeckoProfile get(Context context) {
- return get(context, null, (File) null);
- }
-
- public static GeckoProfile get(Context context, String profileName) {
- if (profileName != null) {
- GeckoProfile profile = sProfileCache.get(profileName);
- if (profile != null)
- return profile;
- }
- return get(context, profileName, (File)null);
- }
-
- @RobocopTarget
- public static GeckoProfile get(Context context, String profileName, String profilePath) {
- File dir = null;
- if (!TextUtils.isEmpty(profilePath)) {
- dir = new File(profilePath);
- if (!dir.exists() || !dir.isDirectory()) {
- Log.w(LOGTAG, "requested profile directory missing: " + profilePath);
- }
- }
- return get(context, profileName, dir);
- }
-
- // Note that the profile cache respects only the profile name!
- // If the directory changes, the returned GeckoProfile instance will be mutated.
- @RobocopTarget
- public static GeckoProfile get(Context context, String profileName, File profileDir) {
- if (context == null) {
- throw new IllegalArgumentException("context must be non-null");
- }
-
- // Null name? | Null dir? | Returned profile
- // ------------------------------------------
- // Yes | Yes | Active profile or default profile.
- // No | Yes | Profile with specified name at default dir.
- // Yes | No | Custom (anonymous) profile with specified dir.
- // No | No | Profile with specified name at specified dir.
-
- if (profileName == null && profileDir == null) {
- // If no profile info was passed in, look for the active profile or a default profile.
- final GeckoProfile profile = GeckoThread.getActiveProfile();
- if (profile != null) {
- return profile;
- }
-
- final String args;
- if (context instanceof Activity) {
- args = IntentUtils.getStringExtraSafe(((Activity) context).getIntent(), "args");
- } else {
- args = null;
- }
-
- return GeckoProfile.initFromArgs(context, args);
-
- } else if (profileName == null) {
- // If only profile dir was passed in, use custom (anonymous) profile.
- profileName = CUSTOM_PROFILE;
-
- } else if (AppConstants.DEBUG_BUILD) {
- Log.v(LOGTAG, "Fetching profile: '" + profileName + "', '" + profileDir + "'");
- }
-
- // We require the profile dir to exist if specified, so create it here if needed.
- final boolean init = profileDir != null && profileDir.mkdirs();
-
- // Actually try to look up the profile.
- GeckoProfile profile = sProfileCache.get(profileName);
- GeckoProfile newProfile = null;
-
- if (profile == null) {
- try {
- newProfile = new GeckoProfile(context, profileName, profileDir);
- } catch (NoMozillaDirectoryException e) {
- // We're unable to do anything sane here.
- throw new RuntimeException(e);
- }
-
- profile = sProfileCache.putIfAbsent(profileName, newProfile);
- }
-
- if (profile == null) {
- profile = newProfile;
-
- } else if (profileDir != null) {
- // We have an existing profile but was given an alternate directory.
- boolean consistent = false;
- try {
- consistent = profile.mProfileDir != null &&
- profile.mProfileDir.getCanonicalPath().equals(profileDir.getCanonicalPath());
- } catch (final IOException e) {
- }
-
- if (!consistent) {
- if (!sAcceptDirectoryChanges || !profileDir.isDirectory()) {
- throw new IllegalStateException(
- "Refusing to reuse profile with a different directory.");
- }
-
- if (AppConstants.RELEASE_BUILD) {
- Log.e(LOGTAG, "Release build trying to switch out profile dir. " +
- "This is an error, but let's do what we can.");
- }
- profile.setDir(profileDir);
- }
- }
-
- if (init) {
- // Initialize the profile directory if we had to create it.
- profile.enqueueInitialization(profileDir);
- }
-
- return profile;
- }
-
- // Currently unused outside of testing.
- @RobocopTarget
- public static boolean removeProfile(final Context context, final GeckoProfile profile) {
- final boolean success = profile.remove();
-
- if (success) {
- // Clear all shared prefs for the given profile.
- GeckoSharedPrefs.forProfileName(context, profile.getName())
- .edit().clear().apply();
- }
-
- return success;
- }
-
- private static File getGuestDir(final Context context) {
- return context.getFileStreamPath(GUEST_PROFILE_DIR);
- }
-
- @RobocopTarget
- public static GeckoProfile getGuestProfile(final Context context) {
- return get(context, CUSTOM_PROFILE, getGuestDir(context));
- }
-
- public static boolean isGuestProfile(final Context context, final String profileName,
- final File profileDir) {
- // Guest profile is just a custom profile with a special path.
- if (profileDir == null || !CUSTOM_PROFILE.equals(profileName)) {
- return false;
- }
-
- try {
- return profileDir.getCanonicalPath().equals(getGuestDir(context).getCanonicalPath());
- } catch (final IOException e) {
- return false;
- }
- }
-
- private GeckoProfile(Context context, String profileName, File profileDir) throws NoMozillaDirectoryException {
- if (profileName == null) {
- throw new IllegalArgumentException("Unable to create GeckoProfile for empty profile name.");
- } else if (CUSTOM_PROFILE.equals(profileName) && profileDir == null) {
- throw new IllegalArgumentException("Custom profile must have a directory");
- }
-
- mApplicationContext = context.getApplicationContext();
- mName = profileName;
- mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
-
- mProfileDir = profileDir;
- if (profileDir != null && !profileDir.isDirectory()) {
- throw new IllegalArgumentException("Profile directory must exist if specified.");
- }
- }
-
- public static boolean shouldUse(final Context context) {
- return GeckoSharedPrefs.forApp(context).getBoolean(GUEST_MODE_PREF, false);
- }
-
- public static void enter(final Context context) {
- GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, true).commit();
- }
-
- public static void leave(final Context context) {
- GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, false).commit();
- }
-
- private void setDir(File dir) {
- if (dir != null && dir.exists() && dir.isDirectory()) {
- synchronized (this) {
- mProfileDir = dir;
- mInGuestMode = null;
- }
- }
- }
-
- @RobocopTarget
- public String getName() {
- return mName;
- }
-
- public boolean isCustomProfile() {
- return CUSTOM_PROFILE.equals(mName);
- }
-
- @RobocopTarget
- public boolean inGuestMode() {
- if (mInGuestMode == null) {
- mInGuestMode = isGuestProfile(GeckoAppShell.getApplicationContext(),
- mName, mProfileDir);
- }
- return mInGuestMode;
- }
-
- /**
- * Retrieves the directory backing the profile. This method acts
- * as a lazy initializer for the GeckoProfile instance.
- */
- @RobocopTarget
- public synchronized File getDir() {
- forceCreate();
- return mProfileDir;
- }
-
- /**
- * Forces profile creation. Consider using {@link #getDir()} to initialize the profile instead - it is the
- * lazy initializer and, for our code reasoning abilities, we should initialize the profile in one place.
- */
- private synchronized GeckoProfile forceCreate() {
- if (mProfileDir != null) {
- return this;
- }
-
- try {
- // Check if a profile with this name already exists.
- try {
- mProfileDir = findProfileDir();
- Log.d(LOGTAG, "Found profile dir.");
- } catch (NoSuchProfileException noSuchProfile) {
- // If it doesn't exist, create it.
- mProfileDir = createProfileDir();
- }
- } catch (IOException ioe) {
- Log.e(LOGTAG, "Error getting profile dir", ioe);
- }
- return this;
- }
-
- public File getFile(String aFile) {
- File f = getDir();
- if (f == null)
- return null;
-
- return new File(f, aFile);
- }
-
- /**
- * Retrieves the Gecko client ID from the filesystem. If the client ID does not exist, we attempt to migrate and
- * persist it from FHR and, if that fails, we attempt to create a new one ourselves.
- *
- * This method assumes the client ID is located in a file at a hard-coded path within the profile. The format of
- * this file is a JSONObject which at the bottom level contains a String -> String mapping containing the client ID.
- *
- * WARNING: the platform provides a JSM to retrieve the client ID [1] and this would be a
- * robust way to access it. However, we don't want to rely on Gecko running in order to get
- * the client ID so instead we access the file this module accesses directly. However, it's
- * possible the format of this file (and the access calls in the jsm) will change, leaving
- * this code to fail. There are tests in TestGeckoProfile to verify the file format but be
- * warned: THIS IS NOT FOOLPROOF.
- *
- * [1]: https://mxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm
- *
- * @throws IOException if the client ID could not be retrieved.
- */
- // Mimics ClientID.jsm – _doLoadClientID.
- @WorkerThread
- public String getClientId() throws IOException {
- try {
- return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
- } catch (final IOException e) {
- // Avoid log spam: don't log the full Exception w/ the stack trace.
- Log.d(LOGTAG, "Could not get client ID - attempting to migrate ID from FHR: " + e.getLocalizedMessage());
- }
-
- String clientIdToWrite;
- try {
- clientIdToWrite = getValidClientIdFromDisk(FHR_CLIENT_ID_FILE_PATH);
- } catch (final IOException e) {
- // Avoid log spam: don't log the full Exception w/ the stack trace.
- Log.d(LOGTAG, "Could not migrate client ID from FHR – creating a new one: " + e.getLocalizedMessage());
- clientIdToWrite = generateNewClientId();
- }
-
- // There is a possibility Gecko is running and the Gecko telemetry implementation decided it's time to generate
- // the client ID, writing client ID underneath us. Since it's highly unlikely (e.g. we run in onStart before
- // Gecko is started), we don't handle that possibility besides writing the ID and then reading from the file
- // again (rather than just returning the value we generated before writing).
- //
- // In the event it does happen, any discrepancy will be resolved after a restart. In the mean time, both this
- // implementation and the Gecko implementation could upload documents with inconsistent IDs.
- //
- // In any case, if we get an exception, intentionally throw - there's nothing more to do here.
- persistClientId(clientIdToWrite);
- return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
- }
-
- protected static String generateNewClientId() {
- return UUID.randomUUID().toString();
- }
-
- /**
- * @return a valid client ID
- * @throws IOException if a valid client ID could not be retrieved
- */
- @WorkerThread
- private String getValidClientIdFromDisk(final String filePath) throws IOException {
- final JSONObject obj = readJSONObjectFromFile(filePath);
- final String clientId = obj.optString(CLIENT_ID_JSON_ATTR);
- if (isClientIdValid(clientId)) {
- return clientId;
- }
- throw new IOException("Received client ID is invalid: " + clientId);
- }
-
- /**
- * Persists the given client ID to disk. This will overwrite any existing files.
- */
- @WorkerThread
- private void persistClientId(final String clientId) throws IOException {
- if (!ensureParentDirs(CLIENT_ID_FILE_PATH)) {
- throw new IOException("Could not create client ID parent directories");
- }
-
- final JSONObject obj = new JSONObject();
- try {
- obj.put(CLIENT_ID_JSON_ATTR, clientId);
- } catch (final JSONException e) {
- throw new IOException("Could not create client ID JSON object", e);
- }
-
- // ClientID.jsm overwrites the file to store the client ID so it's okay if we do it too.
- Log.d(LOGTAG, "Attempting to write new client ID");
- writeFile(CLIENT_ID_FILE_PATH, obj.toString()); // Logs errors within function: ideally we'd throw.
- }
-
- // From ClientID.jsm - isValidClientID.
- public static boolean isClientIdValid(final String clientId) {
- // We could use UUID.fromString but, for consistency, we take the implementation from ClientID.jsm.
- if (TextUtils.isEmpty(clientId)) {
- return false;
- }
- return clientId.matches("(?i:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})");
- }
-
- /**
- * Gets the profile creation date and persists it if it had to be generated.
- *
- * To get this value, we first look in times.json. If that could not be accessed, we
- * return the package's first install date. This is not a perfect solution because a
- * user may have large gap between install time and first use.
- *
- * A more correct algorithm could be the one performed by the JS code in ProfileAge.jsm
- * getOldestProfileTimestamp: walk the tree and return the oldest timestamp on the files
- * within the profile. However, since times.json will only not exist for the small
- * number of really old profiles, we're okay with the package install date compromise for
- * simplicity.
- *
- * @return the profile creation date in the format returned by {@link System#currentTimeMillis()}
- * or -1 if the value could not be persisted.
- */
- @WorkerThread
- public long getAndPersistProfileCreationDate(final Context context) {
- try {
- return getProfileCreationDateFromTimesFile();
- } catch (final IOException e) {
- Log.d(LOGTAG, "Unable to retrieve profile creation date from times.json. Getting from system...");
- final long packageInstallMillis = org.mozilla.gecko.util.ContextUtils.getCurrentPackageInfo(context).firstInstallTime;
- try {
- persistProfileCreationDateToTimesFile(packageInstallMillis);
- } catch (final IOException ioEx) {
- // We return -1 to ensure the profileCreationDate
- // will either be an error (-1) or a consistent value.
- Log.w(LOGTAG, "Unable to persist profile creation date - returning -1");
- return -1;
- }
-
- return packageInstallMillis;
- }
- }
-
- @WorkerThread
- private long getProfileCreationDateFromTimesFile() throws IOException {
- final JSONObject obj = readJSONObjectFromFile(TIMES_PATH);
- try {
- return obj.getLong(PROFILE_CREATION_DATE_JSON_ATTR);
- } catch (final JSONException e) {
- // Don't log to avoid leaking data in JSONObject.
- throw new IOException("Profile creation does not exist in JSONObject");
- }
- }
-
- @WorkerThread
- private void persistProfileCreationDateToTimesFile(final long profileCreationMillis) throws IOException {
- final JSONObject obj = new JSONObject();
- try {
- obj.put(PROFILE_CREATION_DATE_JSON_ATTR, profileCreationMillis);
- } catch (final JSONException e) {
- // Don't log to avoid leaking data in JSONObject.
- throw new IOException("Unable to persist profile creation date to times file");
- }
- Log.d(LOGTAG, "Attempting to write new profile creation date");
- writeFile(TIMES_PATH, obj.toString()); // Ideally we'd throw here too.
- }
-
- /**
- * Updates the state of the old session data file.
- *
- * sessionstore.js should hold the current session, and sessionstore.bak should
- * hold the previous session (where it is used to read the "tabs from last time").
- * If we're not restoring tabs automatically, sessionstore.js needs to be moved to
- * sessionstore.bak, so we can display the correct "tabs from last time".
- * If we *are* restoring tabs, we need to delete outdated copies of sessionstore.bak,
- * so we don't continue showing stale "tabs from last time" indefinitely.
- *
- * @param shouldRestore Pass true if we are automatically restoring last session's tabs.
- */
- public void updateSessionFile(boolean shouldRestore) {
- File sessionFileBackup = getFile(SESSION_FILE_BACKUP);
- if (!shouldRestore) {
- File sessionFile = getFile(SESSION_FILE);
- if (sessionFile != null && sessionFile.exists()) {
- sessionFile.renameTo(sessionFileBackup);
- }
- } else {
- if (sessionFileBackup != null && sessionFileBackup.exists() &&
- System.currentTimeMillis() - sessionFileBackup.lastModified() > MAX_BACKUP_FILE_AGE) {
- sessionFileBackup.delete();
- }
- }
- synchronized (this) {
- mOldSessionDataProcessed = true;
- notifyAll();
- }
- }
-
- public void waitForOldSessionDataProcessing() {
- synchronized (this) {
- while (!mOldSessionDataProcessed) {
- try {
- wait();
- } catch (final InterruptedException e) {
- // Ignore and wait again.
- }
- }
- }
- }
-
- /**
- * Get the string from a session file.
- *
- * The session can either be read from sessionstore.js or sessionstore.bak.
- * In general, sessionstore.js holds the current session, and
- * sessionstore.bak holds the previous session.
- *
- * @param readBackup if true, the session is read from sessionstore.bak;
- * otherwise, the session is read from sessionstore.js
- *
- * @return the session string
- */
- public String readSessionFile(boolean readBackup) {
- File sessionFile = getFile(readBackup ? SESSION_FILE_BACKUP : SESSION_FILE);
-
- try {
- if (sessionFile != null && sessionFile.exists()) {
- return readFile(sessionFile);
- }
- } catch (IOException ioe) {
- Log.e(LOGTAG, "Unable to read session file", ioe);
- }
- return null;
- }
-
- /**
- * Ensures the parent director(y|ies) of the given filename exist by making them
- * if they don't already exist..
- *
- * @param filename The path to the file whose parents should be made directories
- * @return true if the parent directory exists, false otherwise
- */
- @WorkerThread
- protected boolean ensureParentDirs(final String filename) {
- final File file = new File(getDir(), filename);
- final File parentFile = file.getParentFile();
- return parentFile.mkdirs() || parentFile.isDirectory();
- }
-
- public void writeFile(final String filename, final String data) {
- File file = new File(getDir(), filename);
- BufferedWriter bufferedWriter = null;
- try {
- bufferedWriter = new BufferedWriter(new FileWriter(file, false));
- bufferedWriter.write(data);
- } catch (IOException e) {
- Log.e(LOGTAG, "Unable to write to file", e);
- } finally {
- try {
- if (bufferedWriter != null) {
- bufferedWriter.close();
- }
- } catch (IOException e) {
- Log.e(LOGTAG, "Error closing writer while writing to file", e);
- }
- }
- }
-
- @WorkerThread
- public JSONObject readJSONObjectFromFile(final String filename) throws IOException {
- final String fileContents;
- try {
- fileContents = readFile(filename);
- } catch (final IOException e) {
- // Don't log exception to avoid leaking profile path.
- throw new IOException("Could not access given file to retrieve JSONObject");
- }
-
- try {
- return new JSONObject(fileContents);
- } catch (final JSONException e) {
- // Don't log exception to avoid leaking profile path.
- throw new IOException("Could not parse JSON to retrieve JSONObject");
- }
- }
-
- public JSONArray readJSONArrayFromFile(final String filename) {
- String fileContent;
- try {
- fileContent = readFile(filename);
- } catch (IOException expected) {
- return new JSONArray();
- }
-
- JSONArray jsonArray;
- try {
- jsonArray = new JSONArray(fileContent);
- } catch (JSONException e) {
- jsonArray = new JSONArray();
- }
- return jsonArray;
- }
-
- public String readFile(String filename) throws IOException {
- File dir = getDir();
- if (dir == null) {
- throw new IOException("No profile directory found");
- }
- File target = new File(dir, filename);
- return readFile(target);
- }
-
- private String readFile(File target) throws IOException {
- FileReader fr = new FileReader(target);
- try {
- StringBuilder sb = new StringBuilder();
- char[] buf = new char[8192];
- int read = fr.read(buf);
- while (read >= 0) {
- sb.append(buf, 0, read);
- read = fr.read(buf);
- }
- return sb.toString();
- } finally {
- fr.close();
- }
- }
-
- public boolean deleteFileFromProfileDir(String fileName) throws IllegalArgumentException {
- if (TextUtils.isEmpty(fileName)) {
- throw new IllegalArgumentException("Filename cannot be empty.");
- }
- File file = new File(getDir(), fileName);
- return file.delete();
- }
-
- private boolean remove() {
- try {
- synchronized (this) {
- if (mProfileDir != null && mProfileDir.exists()) {
- FileUtils.delete(mProfileDir);
- }
-
- if (isCustomProfile()) {
- // Custom profiles don't have profile.ini sections that we need to remove.
- return true;
- }
-
- try {
- // If findProfileDir() succeeds, it means the profile was created
- // through forceCreate(), so we set mProfileDir to null to enable
- // forceCreate() to create the profile again.
- findProfileDir();
- mProfileDir = null;
-
- } catch (final NoSuchProfileException e) {
- // If findProfileDir() throws, it means the profile was not created
- // through forceCreate(), and we have to preserve mProfileDir because
- // it was given to us. In that case, there's nothing left to do here.
- return true;
- }
- }
-
- final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
- final Hashtable<String, INISection> sections = parser.getSections();
- for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
- final INISection section = e.nextElement();
- String name = section.getStringProperty("Name");
-
- if (name == null || !name.equals(mName)) {
- continue;
- }
-
- if (section.getName().startsWith("Profile")) {
- // ok, we have stupid Profile#-named things. Rename backwards.
- try {
- int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length()));
- String curSection = "Profile" + sectionNumber;
- String nextSection = "Profile" + (sectionNumber + 1);
-
- sections.remove(curSection);
-
- while (sections.containsKey(nextSection)) {
- parser.renameSection(nextSection, curSection);
- sectionNumber++;
-
- curSection = nextSection;
- nextSection = "Profile" + (sectionNumber + 1);
- }
- } catch (NumberFormatException nex) {
- // uhm, malformed Profile thing; we can't do much.
- Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName());
- return false;
- }
- } else {
- // this really shouldn't be the case, but handle it anyway
- parser.removeSection(mName);
- }
-
- break;
- }
-
- parser.write();
- return true;
- } catch (IOException ex) {
- Log.w(LOGTAG, "Failed to remove profile.", ex);
- return false;
- }
- }
-
- /**
- * @return the default profile name for this application, or
- * {@link GeckoProfile#DEFAULT_PROFILE} if none could be found.
- *
- * @throws NoMozillaDirectoryException
- * if the Mozilla directory did not exist and could not be
- * created.
- */
- public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
- // Have we read the default profile from the INI already?
- // Changing the default profile requires a restart, so we don't
- // need to worry about runtime changes.
- if (sDefaultProfileName != null) {
- return sDefaultProfileName;
- }
-
- final String profileName = GeckoProfileDirectories.findDefaultProfileName(context);
- if (profileName == null) {
- // Note that we don't persist this back to profiles.ini.
- sDefaultProfileName = DEFAULT_PROFILE;
- return DEFAULT_PROFILE;
- }
-
- sDefaultProfileName = profileName;
- return sDefaultProfileName;
- }
-
- private File findProfileDir() throws NoSuchProfileException {
- if (isCustomProfile()) {
- return mProfileDir;
- }
- return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
- }
-
- @WorkerThread
- private File createProfileDir() throws IOException {
- if (isCustomProfile()) {
- // Custom profiles must already exist.
- return mProfileDir;
- }
-
- INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
-
- // Salt the name of our requested profile
- String saltedName = GeckoProfileDirectories.saltProfileName(mName);
- File profileDir = new File(mMozillaDir, saltedName);
- while (profileDir.exists()) {
- saltedName = GeckoProfileDirectories.saltProfileName(mName);
- profileDir = new File(mMozillaDir, saltedName);
- }
-
- // Attempt to create the salted profile dir
- if (!profileDir.mkdirs()) {
- throw new IOException("Unable to create profile.");
- }
- Log.d(LOGTAG, "Created new profile dir.");
-
- // Now update profiles.ini
- // If this is the first time its created, we also add a General section
- // look for the first profile number that isn't taken yet
- int profileNum = 0;
- boolean isDefaultSet = false;
- INISection profileSection;
- while ((profileSection = parser.getSection("Profile" + profileNum)) != null) {
- profileNum++;
- if (profileSection.getProperty("Default") != null) {
- isDefaultSet = true;
- }
- }
-
- profileSection = new INISection("Profile" + profileNum);
- profileSection.setProperty("Name", mName);
- profileSection.setProperty("IsRelative", 1);
- profileSection.setProperty("Path", saltedName);
-
- if (parser.getSection("General") == null) {
- INISection generalSection = new INISection("General");
- generalSection.setProperty("StartWithLastProfile", 1);
- parser.addSection(generalSection);
- }
-
- if (!isDefaultSet) {
- // only set as default if this is the first profile we're creating
- profileSection.setProperty("Default", 1);
-
- // We have no intention of stopping this session. The FIRSTRUN session
- // ends when the browsing session/activity has ended. All events
- // during firstrun will be tagged as FIRSTRUN.
- // TODO: local broadcast to tell App that things are happening?
- // Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
- }
-
- parser.addSection(profileSection);
- parser.write();
-
- enqueueInitialization(profileDir);
-
- // Write out profile creation time, mirroring the logic in nsToolkitProfileService.
- try {
- FileOutputStream stream = new FileOutputStream(profileDir.getAbsolutePath() + File.separator + TIMES_PATH);
- OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
- try {
- writer.append("{\"created\": " + System.currentTimeMillis() + "}\n");
- } finally {
- writer.close();
- }
- } catch (Exception e) {
- // Best-effort.
- Log.w(LOGTAG, "Couldn't write " + TIMES_PATH, e);
- }
-
- // Create the client ID file before Gecko starts (we assume this method
- // is called before Gecko starts). If we let Gecko start, the JS telemetry
- // code may try to write to the file at the same time Java does.
- persistClientId(generateNewClientId());
-
- // Initialize pref flag for displaying the start pane for a new profile.
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplicationContext);
- prefs.edit().putBoolean(PREF_FIRSTRUN_ENABLED, true).apply();
-
- return profileDir;
- }
-
- /**
- * This method is called once, immediately before creation of the profile
- * directory completes.
- *
- * It queues up work to be done in the background to prepare the profile,
- * such as adding default bookmarks.
- *
- * This is public for use *from tests only*!
- */
- @RobocopTarget
- public void enqueueInitialization(final File profileDir) {
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfileDirectories.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.io.File;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.util.INIParser;
-import org.mozilla.gecko.util.INISection;
-
-import android.content.Context;
-
-/**
- * <code>GeckoProfileDirectories</code> manages access to mappings from profile
- * names to salted profile directory paths, as well as the default profile name.
- *
- * This class will eventually come to encapsulate the remaining logic embedded
- * in profiles.ini; for now it's a read-only wrapper.
- */
-public class GeckoProfileDirectories {
- @SuppressWarnings("serial")
- public static class NoMozillaDirectoryException extends Exception {
- public NoMozillaDirectoryException(Throwable cause) {
- super(cause);
- }
-
- public NoMozillaDirectoryException(String reason) {
- super(reason);
- }
-
- public NoMozillaDirectoryException(String reason, Throwable cause) {
- super(reason, cause);
- }
- }
-
- @SuppressWarnings("serial")
- public static class NoSuchProfileException extends Exception {
- public NoSuchProfileException(String detailMessage, Throwable cause) {
- super(detailMessage, cause);
- }
-
- public NoSuchProfileException(String detailMessage) {
- super(detailMessage);
- }
- }
-
- private interface INISectionPredicate {
- public boolean matches(INISection section);
- }
-
- private static final String MOZILLA_DIR_NAME = "mozilla";
-
- /**
- * Returns true if the supplied profile entry represents the default profile.
- */
- private static final INISectionPredicate sectionIsDefault = new INISectionPredicate() {
- @Override
- public boolean matches(INISection section) {
- return section.getIntProperty("Default") == 1;
- }
- };
-
- /**
- * Returns true if the supplied profile entry has a 'Name' field.
- */
- private static final INISectionPredicate sectionHasName = new INISectionPredicate() {
- @Override
- public boolean matches(INISection section) {
- final String name = section.getStringProperty("Name");
- return name != null;
- }
- };
-
- @RobocopTarget
- public static INIParser getProfilesINI(File mozillaDir) {
- return new INIParser(new File(mozillaDir, "profiles.ini"));
- }
-
- /**
- * Utility method to compute a salted profile name: eight random alphanumeric
- * characters, followed by a period, followed by the profile name.
- */
- public static String saltProfileName(final String name) {
- if (name == null) {
- throw new IllegalArgumentException("Cannot salt null profile name.");
- }
-
- final String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789";
- final int scale = allowedChars.length();
- final int saltSize = 8;
-
- final StringBuilder saltBuilder = new StringBuilder(saltSize + 1 + name.length());
- for (int i = 0; i < saltSize; i++) {
- saltBuilder.append(allowedChars.charAt((int)(Math.random() * scale)));
- }
- saltBuilder.append('.');
- saltBuilder.append(name);
- return saltBuilder.toString();
- }
-
- /**
- * Return the Mozilla directory within the files directory of the provided
- * context. This should always be the same within a running application.
- *
- * This method is package-scoped so that new {@link GeckoProfile} instances can
- * contextualize themselves.
- *
- * @return a new File object for the Mozilla directory.
- * @throws NoMozillaDirectoryException
- * if the directory did not exist and could not be created.
- */
- @RobocopTarget
- public static File getMozillaDirectory(Context context) throws NoMozillaDirectoryException {
- final File mozillaDir = new File(context.getFilesDir(), MOZILLA_DIR_NAME);
- if (mozillaDir.mkdirs() || mozillaDir.isDirectory()) {
- return mozillaDir;
- }
-
- // Although this leaks a path to the system log, the path is
- // predictable (unlike a profile directory), so this is fine.
- throw new NoMozillaDirectoryException("Unable to create mozilla directory at " + mozillaDir.getAbsolutePath());
- }
-
- /**
- * Discover the default profile name by examining profiles.ini.
- *
- * Package-scoped because {@link GeckoProfile} needs access to it.
- *
- * @return null if there is no "Default" entry in profiles.ini, or the profile
- * name if there is.
- * @throws NoMozillaDirectoryException
- * if the Mozilla directory did not exist and could not be created.
- */
- static String findDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
- final INIParser parser = GeckoProfileDirectories.getProfilesINI(getMozillaDirectory(context));
-
- for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
- final INISection section = e.nextElement();
- if (section.getIntProperty("Default") == 1) {
- return section.getStringProperty("Name");
- }
- }
-
- return null;
- }
-
- static Map<String, String> getDefaultProfile(final File mozillaDir) {
- return getMatchingProfiles(mozillaDir, sectionIsDefault, true);
- }
-
- static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) {
- final INISectionPredicate predicate = new INISectionPredicate() {
- @Override
- public boolean matches(final INISection section) {
- return name.equals(section.getStringProperty("Name"));
- }
- };
- return getMatchingProfiles(mozillaDir, predicate, true);
- }
-
- /**
- * Calls {@link GeckoProfileDirectories#getMatchingProfiles(File, INISectionPredicate, boolean)}
- * with a filter to ensure that all profiles are named.
- */
- static Map<String, String> getAllProfiles(final File mozillaDir) {
- return getMatchingProfiles(mozillaDir, sectionHasName, false);
- }
-
- /**
- * Return a mapping from the names of all matching profiles (that is,
- * profiles appearing in profiles.ini that match the supplied predicate) to
- * their absolute paths on disk.
- *
- * @param mozillaDir
- * a directory containing profiles.ini.
- * @param predicate
- * a predicate to use when evaluating whether to include a
- * particular INI section.
- * @param stopOnSuccess
- * if true, this method will return with the first result that
- * matches the predicate; if false, all matching results are
- * included.
- * @return a {@link Map} from name to path.
- */
- public static Map<String, String> getMatchingProfiles(final File mozillaDir, INISectionPredicate predicate, boolean stopOnSuccess) {
- final HashMap<String, String> result = new HashMap<String, String>();
- final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
-
- for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
- final INISection section = e.nextElement();
- if (predicate == null || predicate.matches(section)) {
- final String name = section.getStringProperty("Name");
- final String pathString = section.getStringProperty("Path");
- final boolean isRelative = section.getIntProperty("IsRelative") == 1;
- final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
- result.put(name, path.getAbsolutePath());
-
- if (stopOnSuccess) {
- return result;
- }
- }
- }
- return result;
- }
-
- public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException {
- // Open profiles.ini to find the correct path.
- final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
-
- for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
- final INISection section = e.nextElement();
- final String name = section.getStringProperty("Name");
- if (name != null && name.equals(profileName)) {
- if (section.getIntProperty("IsRelative") == 1) {
- return new File(mozillaDir, section.getStringProperty("Path"));
- }
- return new File(section.getStringProperty("Path"));
- }
- }
-
- throw new NoSuchProfileException("No profile " + profileName);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoScreenOrientation.java
+++ /dev/null
@@ -1,414 +0,0 @@
-/* -*- 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.content.pm.ActivityInfo;
-import android.content.res.Configuration;
-import android.util.Log;
-import android.view.Surface;
-import android.app.Activity;
-
-import java.util.Arrays;
-import java.util.List;
-
-/*
- * Updates, locks and unlocks the screen orientation.
- *
- * Note: Replaces the OnOrientationChangeListener to avoid redundant rotation
- * event handling.
- */
-public class GeckoScreenOrientation {
- private static final String LOGTAG = "GeckoScreenOrientation";
-
- // Make sure that any change in dom/base/ScreenOrientation.h happens here too.
- public enum ScreenOrientation {
- NONE(0),
- PORTRAIT_PRIMARY(1 << 0),
- PORTRAIT_SECONDARY(1 << 1),
- PORTRAIT(PORTRAIT_PRIMARY.value | PORTRAIT_SECONDARY.value),
- LANDSCAPE_PRIMARY(1 << 2),
- LANDSCAPE_SECONDARY(1 << 3),
- LANDSCAPE(LANDSCAPE_PRIMARY.value | LANDSCAPE_SECONDARY.value),
- DEFAULT(1 << 4);
-
- public final short value;
-
- private ScreenOrientation(int value) {
- this.value = (short)value;
- }
-
- private final static ScreenOrientation[] sValues = ScreenOrientation.values();
-
- public static ScreenOrientation get(int value) {
- for (ScreenOrientation orient: sValues) {
- if (orient.value == value) {
- return orient;
- }
- }
- return NONE;
- }
- }
-
- // Singleton instance.
- private static GeckoScreenOrientation sInstance;
- // Default screen orientation, used for initialization and unlocking.
- private static final ScreenOrientation DEFAULT_SCREEN_ORIENTATION = ScreenOrientation.DEFAULT;
- // Default rotation, used when device rotation is unknown.
- private static final int DEFAULT_ROTATION = Surface.ROTATION_0;
- // Default orientation, used if screen orientation is unspecified.
- private ScreenOrientation mDefaultScreenOrientation;
- // Last updated screen orientation.
- private ScreenOrientation mScreenOrientation;
- // Whether the update should notify Gecko about screen orientation changes.
- private boolean mShouldNotify = true;
- // Configuration screen orientation preference path.
- private static final String DEFAULT_SCREEN_ORIENTATION_PREF = "app.orientation.default";
-
- public GeckoScreenOrientation() {
- PrefsHelper.getPref(DEFAULT_SCREEN_ORIENTATION_PREF, new PrefsHelper.PrefHandlerBase() {
- @Override public void prefValue(String pref, String value) {
- // Read and update the configuration default preference.
- mDefaultScreenOrientation = screenOrientationFromArrayString(value);
- setRequestedOrientation(mDefaultScreenOrientation);
- }
- });
-
- mDefaultScreenOrientation = DEFAULT_SCREEN_ORIENTATION;
- update();
- }
-
- public static GeckoScreenOrientation getInstance() {
- if (sInstance == null) {
- sInstance = new GeckoScreenOrientation();
- }
- return sInstance;
- }
-
- /*
- * Enable Gecko screen orientation events on update.
- */
- public void enableNotifications() {
- update();
- mShouldNotify = true;
- }
-
- /*
- * Disable Gecko screen orientation events on update.
- */
- public void disableNotifications() {
- mShouldNotify = false;
- }
-
- /*
- * Update screen orientation.
- * Retrieve orientation and rotation via GeckoAppShell.
- *
- * @return Whether the screen orientation has changed.
- */
- public boolean update() {
- Activity activity = getActivity();
- if (activity == null) {
- return false;
- }
- Configuration config = activity.getResources().getConfiguration();
- return update(config.orientation);
- }
-
- /*
- * Update screen orientation given the android orientation.
- * Retrieve rotation via GeckoAppShell.
- *
- * @param aAndroidOrientation
- * Android screen orientation from Configuration.orientation.
- *
- * @return Whether the screen orientation has changed.
- */
- public boolean update(int aAndroidOrientation) {
- return update(getScreenOrientation(aAndroidOrientation, getRotation()));
- }
-
- /*
- * Update screen orientation given the screen orientation.
- *
- * @param aScreenOrientation
- * Gecko screen orientation based on android orientation and rotation.
- *
- * @return Whether the screen orientation has changed.
- */
- public boolean update(ScreenOrientation aScreenOrientation) {
- if (mScreenOrientation == aScreenOrientation) {
- return false;
- }
- mScreenOrientation = aScreenOrientation;
- Log.d(LOGTAG, "updating to new orientation " + mScreenOrientation);
- if (mShouldNotify) {
- // Gecko expects a definite screen orientation, so we default to the
- // primary orientations.
- if (aScreenOrientation == ScreenOrientation.PORTRAIT) {
- aScreenOrientation = ScreenOrientation.PORTRAIT_PRIMARY;
- } else if (aScreenOrientation == ScreenOrientation.LANDSCAPE) {
- aScreenOrientation = ScreenOrientation.LANDSCAPE_PRIMARY;
- }
- GeckoAppShell.sendEventToGecko(
- GeckoEvent.createScreenOrientationEvent(aScreenOrientation.value,
- getAngle()));
- }
- GeckoAppShell.resetScreenSize();
- return true;
- }
-
- /*
- * @return The Android orientation (Configuration.orientation).
- */
- public int getAndroidOrientation() {
- return screenOrientationToAndroidOrientation(getScreenOrientation());
- }
-
- /*
- * @return The Gecko screen orientation derived from Android orientation and
- * rotation.
- */
- public ScreenOrientation getScreenOrientation() {
- return mScreenOrientation;
- }
-
- /*
- * Lock screen orientation given the Gecko screen orientation.
- *
- * @param aGeckoOrientation
- * The Gecko orientation provided.
- */
- public void lock(int aGeckoOrientation) {
- lock(ScreenOrientation.get(aGeckoOrientation));
- }
-
- /*
- * Lock screen orientation given the Gecko screen orientation.
- * Retrieve rotation via GeckoAppShell.
- *
- * @param aScreenOrientation
- * Gecko screen orientation derived from Android orientation and
- * rotation.
- *
- * @return Whether the locking was successful.
- */
- public boolean lock(ScreenOrientation aScreenOrientation) {
- Log.d(LOGTAG, "locking to " + aScreenOrientation);
- update(aScreenOrientation);
- return setRequestedOrientation(aScreenOrientation);
- }
-
- /*
- * Unlock and update screen orientation.
- *
- * @return Whether the unlocking was successful.
- */
- public boolean unlock() {
- Log.d(LOGTAG, "unlocking");
- setRequestedOrientation(mDefaultScreenOrientation);
- return update();
- }
-
- private Activity getActivity() {
- if (GeckoAppShell.getGeckoInterface() == null) {
- return null;
- }
- return GeckoAppShell.getGeckoInterface().getActivity();
- }
-
- /*
- * Set the given requested orientation for the current activity.
- * This is essentially an unlock without an update.
- *
- * @param aScreenOrientation
- * Gecko screen orientation.
- *
- * @return Whether the requested orientation was set. This can only fail if
- * the current activity cannot be retrieved via GeckoAppShell.
- *
- */
- private boolean setRequestedOrientation(ScreenOrientation aScreenOrientation) {
- int activityOrientation = screenOrientationToActivityInfoOrientation(aScreenOrientation);
- Activity activity = getActivity();
- if (activity == null) {
- Log.w(LOGTAG, "setRequestOrientation: failed to get activity");
- return false;
- }
- if (activity.getRequestedOrientation() == activityOrientation) {
- return false;
- }
- activity.setRequestedOrientation(activityOrientation);
- return true;
- }
-
- /*
- * Combine the Android orientation and rotation to the Gecko orientation.
- *
- * @param aAndroidOrientation
- * Android orientation from Configuration.orientation.
- * @param aRotation
- * Device rotation from Display.getRotation().
- *
- * @return Gecko screen orientation.
- */
- private ScreenOrientation getScreenOrientation(int aAndroidOrientation, int aRotation) {
- boolean isPrimary = aRotation == Surface.ROTATION_0 || aRotation == Surface.ROTATION_90;
- if (aAndroidOrientation == Configuration.ORIENTATION_PORTRAIT) {
- if (isPrimary) {
- // Non-rotated portrait device or landscape device rotated
- // to primary portrait mode counter-clockwise.
- return ScreenOrientation.PORTRAIT_PRIMARY;
- }
- return ScreenOrientation.PORTRAIT_SECONDARY;
- }
- if (aAndroidOrientation == Configuration.ORIENTATION_LANDSCAPE) {
- if (isPrimary) {
- // Non-rotated landscape device or portrait device rotated
- // to primary landscape mode counter-clockwise.
- return ScreenOrientation.LANDSCAPE_PRIMARY;
- }
- return ScreenOrientation.LANDSCAPE_SECONDARY;
- }
- return ScreenOrientation.NONE;
- }
-
- /*
- * @return Device rotation converted to an angle.
- */
- public short getAngle() {
- switch (getRotation()) {
- case Surface.ROTATION_0:
- return 0;
- case Surface.ROTATION_90:
- return 90;
- case Surface.ROTATION_180:
- return 180;
- case Surface.ROTATION_270:
- return 270;
- default:
- Log.w(LOGTAG, "getAngle: unexpected rotation value");
- return 0;
- }
- }
-
- /*
- * @return Device rotation from Display.getRotation().
- */
- private int getRotation() {
- Activity activity = getActivity();
- if (activity == null) {
- Log.w(LOGTAG, "getRotation: failed to get activity");
- return DEFAULT_ROTATION;
- }
- return activity.getWindowManager().getDefaultDisplay().getRotation();
- }
-
- /*
- * Retrieve the screen orientation from an array string.
- *
- * @param aArray
- * String containing comma-delimited strings.
- *
- * @return First parsed Gecko screen orientation.
- */
- public static ScreenOrientation screenOrientationFromArrayString(String aArray) {
- List<String> orientations = Arrays.asList(aArray.split(","));
- if (orientations.size() == 0) {
- // If nothing is listed, return default.
- Log.w(LOGTAG, "screenOrientationFromArrayString: no orientation in string");
- return DEFAULT_SCREEN_ORIENTATION;
- }
-
- // We don't support multiple orientations yet. To avoid developer
- // confusion, just take the first one listed.
- return screenOrientationFromString(orientations.get(0));
- }
-
- /*
- * Retrieve the screen orientation from a string.
- *
- * @param aStr
- * String hopefully containing a screen orientation name.
- * @return Gecko screen orientation if matched, DEFAULT_SCREEN_ORIENTATION
- * otherwise.
- */
- public static ScreenOrientation screenOrientationFromString(String aStr) {
- switch (aStr) {
- case "portrait":
- return ScreenOrientation.PORTRAIT;
- case "landscape":
- return ScreenOrientation.LANDSCAPE;
- case "portrait-primary":
- return ScreenOrientation.PORTRAIT_PRIMARY;
- case "portrait-secondary":
- return ScreenOrientation.PORTRAIT_SECONDARY;
- case "landscape-primary":
- return ScreenOrientation.LANDSCAPE_PRIMARY;
- case "landscape-secondary":
- return ScreenOrientation.LANDSCAPE_SECONDARY;
- }
-
- Log.w(LOGTAG, "screenOrientationFromString: unknown orientation string: " + aStr);
- return DEFAULT_SCREEN_ORIENTATION;
- }
-
- /*
- * Convert Gecko screen orientation to Android orientation.
- *
- * @param aScreenOrientation
- * Gecko screen orientation.
- * @return Android orientation. This conversion is lossy, the Android
- * orientation does not differentiate between primary and secondary
- * orientations.
- */
- public static int screenOrientationToAndroidOrientation(ScreenOrientation aScreenOrientation) {
- switch (aScreenOrientation) {
- case PORTRAIT:
- case PORTRAIT_PRIMARY:
- case PORTRAIT_SECONDARY:
- return Configuration.ORIENTATION_PORTRAIT;
- case LANDSCAPE:
- case LANDSCAPE_PRIMARY:
- case LANDSCAPE_SECONDARY:
- return Configuration.ORIENTATION_LANDSCAPE;
- case NONE:
- case DEFAULT:
- default:
- return Configuration.ORIENTATION_UNDEFINED;
- }
- }
-
-
- /*
- * Convert Gecko screen orientation to Android ActivityInfo orientation.
- * This is yet another orientation used by Android, but it's more detailed
- * than the Android orientation.
- * It is required for screen orientation locking and unlocking.
- *
- * @param aScreenOrientation
- * Gecko screen orientation.
- * @return Android ActivityInfo orientation.
- */
- public static int screenOrientationToActivityInfoOrientation(ScreenOrientation aScreenOrientation) {
- switch (aScreenOrientation) {
- case PORTRAIT:
- case PORTRAIT_PRIMARY:
- return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
- case PORTRAIT_SECONDARY:
- return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
- case LANDSCAPE:
- case LANDSCAPE_PRIMARY:
- return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
- case LANDSCAPE_SECONDARY:
- return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
- case DEFAULT:
- case NONE:
- return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
- default:
- return ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-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.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();
- GeckoAppShell.setApplicationContext(getApplicationContext());
- GeckoAppShell.setNotificationClient(new ServiceNotificationClient(getApplicationContext()));
- 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 || profileDir == null) {
- throw new IllegalArgumentException("Intent must specify profile.");
- }
-
- if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
- new File(profileDir))) {
- 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;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoSharedPrefs.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/* 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.Arrays;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.os.StrictMode;
-import android.preference.PreferenceManager;
-import android.util.Log;
-
-/**
- * {@code GeckoSharedPrefs} provides scoped SharedPreferences instances.
- * You should use this API instead of using Context.getSharedPreferences()
- * directly. There are four methods to get scoped SharedPreferences instances:
- *
- * forApp()
- * Use it for app-wide, cross-profile pref keys.
- * forCrashReporter()
- * For the crash reporter, which runs in its own process.
- * forProfile()
- * Use it to fetch and store keys for the current profile.
- * forProfileName()
- * Use it to fetch and store keys from/for a specific profile.
- *
- * {@code GeckoSharedPrefs} has a notion of migrations. Migrations can used to
- * migrate keys from one scope to another. You can trigger a new migration by
- * incrementing PREFS_VERSION and updating migrateIfNecessary() accordingly.
- *
- * Migration history:
- * 1: Move all PreferenceManager keys to app/profile scopes
- * 2: Move the crash reporter's private preferences into their own scope
- */
-@RobocopTarget
-public final class GeckoSharedPrefs {
- private static final String LOGTAG = "GeckoSharedPrefs";
-
- // Increment it to trigger a new migration
- public static final int PREFS_VERSION = 2;
-
- // Name for app-scoped prefs
- public static final String APP_PREFS_NAME = "GeckoApp";
-
- // Name for crash reporter prefs
- public static final String CRASH_PREFS_NAME = "CrashReporter";
-
- // Used when fetching profile-scoped prefs.
- public static final String PROFILE_PREFS_NAME_PREFIX = "GeckoProfile-";
-
- // The prefs key that holds the current migration
- private static final String PREFS_VERSION_KEY = "gecko_shared_prefs_migration";
-
- // For disabling migration when getting a SharedPreferences instance
- private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS);
-
- // The keys that have to be moved from ProfileManager's default
- // shared prefs to the profile from version 0 to 1.
- private static final String[] PROFILE_MIGRATIONS_0_TO_1 = {
- "home_panels",
- "home_locale"
- };
-
- // The keys that have to be moved from the app prefs
- // into the crash reporter's own prefs.
- private static final String[] PROFILE_MIGRATIONS_1_TO_2 = {
- "sendReport",
- "includeUrl",
- "allowContact",
- "contactEmail"
- };
-
- // For optimizing the migration check in subsequent get() calls
- private static volatile boolean migrationDone;
-
- public enum Flags {
- DISABLE_MIGRATIONS
- }
-
- public static SharedPreferences forApp(Context context) {
- return forApp(context, EnumSet.noneOf(Flags.class));
- }
-
- /**
- * Returns an app-scoped SharedPreferences instance. You can disable
- * migrations by using the DISABLE_MIGRATIONS flag.
- */
- public static SharedPreferences forApp(Context context, EnumSet<Flags> flags) {
- if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
- migrateIfNecessary(context);
- }
-
- return context.getSharedPreferences(APP_PREFS_NAME, 0);
- }
-
- public static SharedPreferences forCrashReporter(Context context) {
- return forCrashReporter(context, EnumSet.noneOf(Flags.class));
- }
-
- /**
- * Returns a crash-reporter-scoped SharedPreferences instance. You can disable
- * migrations by using the DISABLE_MIGRATIONS flag.
- */
- public static SharedPreferences forCrashReporter(Context context, EnumSet<Flags> flags) {
- if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
- migrateIfNecessary(context);
- }
-
- return context.getSharedPreferences(CRASH_PREFS_NAME, 0);
- }
-
- public static SharedPreferences forProfile(Context context) {
- return forProfile(context, EnumSet.noneOf(Flags.class));
- }
-
- /**
- * Returns a SharedPreferences instance scoped to the current profile
- * in the app. You can disable migrations by using the DISABLE_MIGRATIONS
- * flag.
- */
- public static SharedPreferences forProfile(Context context, EnumSet<Flags> flags) {
- String profileName = GeckoProfile.get(context).getName();
- if (profileName == null) {
- throw new IllegalStateException("Could not get current profile name");
- }
-
- return forProfileName(context, profileName, flags);
- }
-
- public static SharedPreferences forProfileName(Context context, String profileName) {
- return forProfileName(context, profileName, EnumSet.noneOf(Flags.class));
- }
-
- /**
- * Returns an SharedPreferences instance scoped to the given profile name.
- * You can disable migrations by using the DISABLE_MIGRATION flag.
- */
- public static SharedPreferences forProfileName(Context context, String profileName,
- EnumSet<Flags> flags) {
- if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
- migrateIfNecessary(context);
- }
-
- final String prefsName = PROFILE_PREFS_NAME_PREFIX + profileName;
- return context.getSharedPreferences(prefsName, 0);
- }
-
- /**
- * Returns the current version of the prefs.
- */
- public static int getVersion(Context context) {
- return forApp(context, disableMigrations).getInt(PREFS_VERSION_KEY, 0);
- }
-
- /**
- * Resets migration flag. Should only be used in tests.
- */
- public static synchronized void reset() {
- migrationDone = false;
- }
-
- /**
- * Performs all prefs migrations in the background thread to avoid StrictMode
- * exceptions from reading/writing in the UI thread. This method will block
- * the current thread until the migration is finished.
- */
- private static synchronized void migrateIfNecessary(final Context context) {
- if (migrationDone) {
- return;
- }
-
- // We deliberately perform the migration in the current thread (which
- // is likely the UI thread) as this is actually cheaper than enforcing a
- // context switch to another thread (see bug 940575).
- // Avoid strict mode warnings when doing so.
- final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
- StrictMode.allowThreadDiskWrites();
- try {
- performMigration(context);
- } finally {
- StrictMode.setThreadPolicy(savedPolicy);
- }
-
- migrationDone = true;
- }
-
- private static void performMigration(Context context) {
- final SharedPreferences appPrefs = forApp(context, disableMigrations);
-
- final int currentVersion = appPrefs.getInt(PREFS_VERSION_KEY, 0);
- Log.d(LOGTAG, "Current version = " + currentVersion + ", prefs version = " + PREFS_VERSION);
-
- if (currentVersion == PREFS_VERSION) {
- return;
- }
-
- Log.d(LOGTAG, "Performing migration");
-
- final Editor appEditor = appPrefs.edit();
-
- // The migration always moves prefs to the default profile, not
- // the current one. We might have to revisit this if we ever support
- // multiple profiles.
- final String defaultProfileName;
- try {
- defaultProfileName = GeckoProfile.getDefaultProfileName(context);
- } catch (Exception e) {
- throw new IllegalStateException("Failed to get default profile name for migration");
- }
-
- final Editor profileEditor = forProfileName(context, defaultProfileName, disableMigrations).edit();
- final Editor crashEditor = forCrashReporter(context, disableMigrations).edit();
-
- List<String> profileKeys;
- Editor pmEditor = null;
-
- for (int v = currentVersion + 1; v <= PREFS_VERSION; v++) {
- Log.d(LOGTAG, "Migrating to version = " + v);
-
- switch (v) {
- case 1:
- profileKeys = Arrays.asList(PROFILE_MIGRATIONS_0_TO_1);
- pmEditor = migrateFromPreferenceManager(context, appEditor, profileEditor, profileKeys);
- break;
- case 2:
- profileKeys = Arrays.asList(PROFILE_MIGRATIONS_1_TO_2);
- migrateCrashReporterSettings(appPrefs, appEditor, crashEditor, profileKeys);
- break;
- }
- }
-
- // Update prefs version accordingly.
- appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION);
-
- appEditor.apply();
- profileEditor.apply();
- crashEditor.apply();
- if (pmEditor != null) {
- pmEditor.apply();
- }
-
- Log.d(LOGTAG, "All keys have been migrated");
- }
-
- /**
- * Moves all preferences stored in PreferenceManager's default prefs
- * to either app or profile scopes. The profile-scoped keys are defined
- * in given profileKeys list, all other keys are moved to the app scope.
- */
- public static Editor migrateFromPreferenceManager(Context context, Editor appEditor,
- Editor profileEditor, List<String> profileKeys) {
- Log.d(LOGTAG, "Migrating from PreferenceManager");
-
- final SharedPreferences pmPrefs =
- PreferenceManager.getDefaultSharedPreferences(context);
-
- for (Map.Entry<String, ?> entry : pmPrefs.getAll().entrySet()) {
- final String key = entry.getKey();
-
- final Editor to;
- if (profileKeys.contains(key)) {
- to = profileEditor;
- } else {
- to = appEditor;
- }
-
- putEntry(to, key, entry.getValue());
- }
-
- // Clear PreferenceManager's prefs once we're done
- // and return the Editor to be committed.
- return pmPrefs.edit().clear();
- }
-
- /**
- * Moves the crash reporter's preferences from the app-wide prefs
- * into its own shared prefs to avoid cross-process pref accesses.
- */
- public static void migrateCrashReporterSettings(SharedPreferences appPrefs, Editor appEditor,
- Editor crashEditor, List<String> profileKeys) {
- Log.d(LOGTAG, "Migrating crash reporter settings");
-
- for (Map.Entry<String, ?> entry : appPrefs.getAll().entrySet()) {
- final String key = entry.getKey();
-
- if (profileKeys.contains(key)) {
- putEntry(crashEditor, key, entry.getValue());
- appEditor.remove(key);
- }
- }
- }
-
- private static void putEntry(Editor to, String key, Object value) {
- Log.d(LOGTAG, "Migrating key = " + key + " with value = " + value);
-
- if (value instanceof String) {
- to.putString(key, (String) value);
- } else if (value instanceof Boolean) {
- to.putBoolean(key, (Boolean) value);
- } else if (value instanceof Long) {
- to.putLong(key, (Long) value);
- } else if (value instanceof Float) {
- to.putFloat(key, (Float) value);
- } else if (value instanceof Integer) {
- to.putInt(key, (Integer) value);
- } else {
- throw new IllegalStateException("Unrecognized value type for key: " + key);
- }
- }
-}
\ No newline at end of file
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoSmsManager.java
+++ /dev/null
@@ -1,1244 +0,0 @@
-/* -*- 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 android.app.Activity;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.provider.Telephony;
-import android.telephony.SmsManager;
-import android.telephony.SmsMessage;
-import android.util.Log;
-
-import static android.telephony.SmsMessage.MessageClass;
-import static org.mozilla.gecko.SmsManager.ISmsManager;
-
-import java.util.ArrayList;
-import java.util.Formatter;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * The envelope class contains all information that are needed to keep track of
- * a sent SMS.
- */
-class Envelope
-{
- enum SubParts {
- SENT_PART,
- DELIVERED_PART
- }
-
- protected int mId;
- protected int mMessageId;
- protected long mMessageTimestamp;
-
- /**
- * Number of sent/delivered remaining parts.
- * @note The array has much slots as SubParts items.
- */
- protected int[] mRemainingParts;
-
- /**
- * Whether sending/delivering is currently failing.
- * @note The array has much slots as SubParts items.
- */
- protected boolean[] mFailing;
-
- /**
- * Error type (only for sent).
- */
- protected int mError;
-
- public Envelope(int aId, int aParts) {
- mId = aId;
- mMessageId = -1;
- mError = GeckoSmsManager.kNoError;
-
- int size = SubParts.values().length;
- mRemainingParts = new int[size];
- mFailing = new boolean[size];
-
- for (int i = 0; i < size; ++i) {
- mRemainingParts[i] = aParts;
- mFailing[i] = false;
- }
- }
-
- public void decreaseRemainingParts(SubParts aType) {
- --mRemainingParts[aType.ordinal()];
-
- if (mRemainingParts[SubParts.SENT_PART.ordinal()] >
- mRemainingParts[SubParts.DELIVERED_PART.ordinal()]) {
- Log.e("GeckoSmsManager", "Delivered more parts than we sent!?");
- }
- }
-
- public boolean arePartsRemaining(SubParts aType) {
- return mRemainingParts[aType.ordinal()] != 0;
- }
-
- public void markAsFailed(SubParts aType) {
- mFailing[aType.ordinal()] = true;
- }
-
- public boolean isFailing(SubParts aType) {
- return mFailing[aType.ordinal()];
- }
-
- public int getMessageId() {
- return mMessageId;
- }
-
- public void setMessageId(int aMessageId) {
- mMessageId = aMessageId;
- }
-
- public long getMessageTimestamp() {
- return mMessageTimestamp;
- }
-
- public void setMessageTimestamp(long aMessageTimestamp) {
- mMessageTimestamp = aMessageTimestamp;
- }
-
- public int getError() {
- return mError;
- }
-
- public void setError(int aError) {
- mError = aError;
- }
-}
-
-/**
- * Postman class is a singleton that manages Envelope instances.
- */
-class Postman
-{
- public static final int kUnknownEnvelopeId = -1;
-
- private static final Postman sInstance = new Postman();
-
- private final ArrayList<Envelope> mEnvelopes = new ArrayList<>(1);
-
- private Postman() {}
-
- public static Postman getInstance() {
- return sInstance;
- }
-
- public int createEnvelope(int aParts) {
- /*
- * We are going to create the envelope in the first empty slot in the array
- * list. If there is no empty slot, we create a new one.
- */
- int size = mEnvelopes.size();
-
- for (int i = 0; i < size; ++i) {
- if (mEnvelopes.get(i) == null) {
- mEnvelopes.set(i, new Envelope(i, aParts));
- return i;
- }
- }
-
- mEnvelopes.add(new Envelope(size, aParts));
- return size;
- }
-
- public Envelope getEnvelope(int aId) {
- if (aId < 0 || mEnvelopes.size() <= aId) {
- Log.e("GeckoSmsManager", "Trying to get an unknown Envelope!");
- return null;
- }
-
- Envelope envelope = mEnvelopes.get(aId);
- if (envelope == null) {
- Log.e("GeckoSmsManager", "Trying to get an empty Envelope!");
- }
-
- return envelope;
- }
-
- public void destroyEnvelope(int aId) {
- if (aId < 0 || mEnvelopes.size() <= aId) {
- Log.e("GeckoSmsManager", "Trying to destroy an unknown Envelope!");
- return;
- }
-
- if (mEnvelopes.set(aId, null) == null) {
- Log.e("GeckoSmsManager", "Trying to destroy an empty Envelope!");
- }
- }
-}
-
-class SmsIOThread extends HandlerThread {
- private final static SmsIOThread sInstance = new SmsIOThread();
-
- private Handler mHandler;
-
- public static SmsIOThread getInstance() {
- return sInstance;
- }
-
- SmsIOThread() {
- super("SmsIOThread");
- }
-
- @Override
- public void start() {
- super.start();
- mHandler = new Handler(getLooper());
- }
-
- public boolean execute(Runnable r) {
- return mHandler.post(r);
- }
-}
-
-class MessagesListManager
-{
- private static final MessagesListManager sInstance = new MessagesListManager();
-
- public static MessagesListManager getInstance() {
- return sInstance;
- }
-
- private final HashMap<Integer, Cursor> mCursors = new HashMap<>();
-
- public void add(int id, Cursor aCursor) {
- if (mCursors.containsKey(id)) {
- Log.e("GeckoSmsManager", "Trying to overwrite cursor!");
- return;
- }
-
- mCursors.put(id, aCursor);
- }
-
- public Cursor get(int aId) {
- if (!mCursors.containsKey(aId)) {
- Log.e("GeckoSmsManager", "Cursor doesn't exist!");
- return null;
- }
-
- return mCursors.get(aId);
- }
-
- public void remove(int aId) {
- if (!mCursors.containsKey(aId)) {
- Log.e("GeckoSmsManager", "Cursor doesn't exist!");
- return;
- }
-
- mCursors.remove(aId);
- }
-
- public void clear() {
- Set<Map.Entry<Integer, Cursor>> entries = mCursors.entrySet();
- Iterator<Map.Entry<Integer, Cursor>> it = entries.iterator();
- while (it.hasNext()) {
- it.next().getValue().close();
- }
-
- mCursors.clear();
- }
-}
-
-public class GeckoSmsManager
- extends BroadcastReceiver
- implements ISmsManager
-{
- public final static String ACTION_SMS_SENT = "org.mozilla.gecko.SMS_SENT";
- public final static String ACTION_SMS_DELIVERED = "org.mozilla.gecko.SMS_DELIVERED";
-
- /*
- * Make sure that the following error codes are in sync with |ErrorType| in:
- * dom/mobilemessage/interfaces/nsIMobileMessageCallback.idl
- * The error codes are owned by the DOM.
- */
- public final static int kNoError = 0;
- public final static int kNoSignalError = 1;
- public final static int kNotFoundError = 2;
- public final static int kUnknownError = 3;
- public final static int kInternalError = 4;
- public final static int kNoSimCardError = 5;
- public final static int kRadioDisabledError = 6;
- public final static int kInvalidAddressError = 7;
- public final static int kFdnCheckError = 8;
- public final static int kNonActiveSimCardError = 9;
- public final static int kStorageFullError = 10;
- public final static int kSimNotMatchedError = 11;
- public final static int kNetworkProblemsError = 12;
- public final static int kGeneralProblemsError = 13;
- public final static int kServiceNotAvailableError = 14;
- public final static int kMessageTooLongForNetworkError = 15;
- public final static int kServiceNotSupportedError = 16;
- public final static int kRetryRequiredError = 17;
-
- private final static int kMaxMessageSize = 160;
-
- private final static Uri kSmsContentUri = Telephony.Sms.Inbox.CONTENT_URI;
- private final static Uri kSmsSentContentUri = Telephony.Sms.Sent.CONTENT_URI;
- private final static Uri kSmsThreadsContentUri = Telephony.Sms.Conversations.CONTENT_URI;
-
- private final static int kSmsTypeInbox = Telephony.Sms.MESSAGE_TYPE_INBOX;
- private final static int kSmsTypeSentbox = Telephony.Sms.MESSAGE_TYPE_SENT;
-
- /*
- * Keep the following state codes in syng with |DeliveryState| in:
- * dom/mobilemessage/Types.h
- */
- private final static int kDeliveryStateSent = 0;
- private final static int kDeliveryStateReceived = 1;
- private final static int kDeliveryStateSending = 2;
- private final static int kDeliveryStateError = 3;
- private final static int kDeliveryStateUnknown = 4;
- private final static int kDeliveryStateNotDownloaded = 5;
- private final static int kDeliveryStateEndGuard = 6;
-
- /*
- * Keep the following status codes in sync with |DeliveryStatus| in:
- * dom/mobilemessage/Types.h
- */
- private final static int kDeliveryStatusNotApplicable = 0;
- private final static int kDeliveryStatusSuccess = 1;
- private final static int kDeliveryStatusPending = 2;
- private final static int kDeliveryStatusError = 3;
-
- /*
- * Keep the following values in sync with |MessageClass| in:
- * dom/mobilemessage/Types.h
- */
- private final static int kMessageClassNormal = 0;
- private final static int kMessageClassClass0 = 1;
- private final static int kMessageClassClass1 = 2;
- private final static int kMessageClassClass2 = 3;
- private final static int kMessageClassClass3 = 4;
-
- private final static String[] kRequiredMessageRows = { "_id", "address", "body", "date", "type", "status", "read", "thread_id" };
- private final static String[] kRequiredMessageRowsForThread = { "_id", "address", "body", "read", "subject", "date" };
- private final static String[] kThreadProjection = { "thread_id" };
-
- // Used to generate monotonically increasing GUIDs.
- private static final AtomicInteger pendingIntentGuid = new AtomicInteger(Integer.MIN_VALUE);
-
- // The maximum value of a 32 bit signed integer. Used to enforce a limit on ids.
- private static final long UNSIGNED_INTEGER_MAX_VALUE = Integer.MAX_VALUE * 2L + 1L;
-
- public GeckoSmsManager() {
- if (SmsIOThread.getInstance().getState() == Thread.State.NEW) {
- SmsIOThread.getInstance().start();
- }
- }
-
- private boolean isDefaultSmsApp(Context context) {
- String myPackageName = context.getPackageName();
- return Telephony.Sms.getDefaultSmsPackage(context).equals(myPackageName);
- }
-
- @Override
- public void start() {
- IntentFilter smsFilter = new IntentFilter();
- smsFilter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
- smsFilter.addAction(ACTION_SMS_SENT);
- smsFilter.addAction(ACTION_SMS_DELIVERED);
-
- GeckoAppShell.getContext().registerReceiver(this, smsFilter);
- }
-
- /**
- * Build up the SMS message body from the SmsMessage array of received SMS
- *
- * @param msgs The SmsMessage array of the received SMS
- * @return The text message body
- */
- private static String buildMessageBodyFromPdus(SmsMessage[] msgs) {
- if (msgs.length == 1) {
- // There is only one part, so grab the body directly.
- return replaceFormFeeds(msgs[0].getDisplayMessageBody());
- } else {
- // Build up the body from the parts.
- StringBuilder body = new StringBuilder();
- for (SmsMessage msg : msgs) {
- // getDisplayMessageBody() can NPE if mWrappedMessage inside is null.
- body.append(msg.getDisplayMessageBody());
- }
- return replaceFormFeeds(body.toString());
- }
- }
-
- // Some providers send formfeeds in their messages. Convert those formfeeds to newlines.
- private static String replaceFormFeeds(String s) {
- return s == null ? "" : s.replace('\f', '\n');
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION) ||
- intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
- // If we're the default SMS, ignore SMS_RECEIVED intents since we'll handle
- // the SMS_DELIVER intent instead.
- if (isDefaultSmsApp(GeckoAppShell.getContext()) && intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
- return;
- }
-
- // TODO: Try to find the receiver number to be able to populate
- // SmsMessage.receiver.
- SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
- if (messages == null || messages.length == 0) {
- return;
- }
-
- SmsMessage sms = messages[0];
- String body = buildMessageBodyFromPdus(messages);
- long timestamp = System.currentTimeMillis();
-
- int id = 0;
- // We only need to save the message if we're the default SMS app
- if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION)) {
- id = saveReceivedMessage(context,
- sms.getDisplayOriginatingAddress(),
- body,
- sms.getTimestampMillis(),
- timestamp,
- sms.getPseudoSubject());
- }
-
- notifySmsReceived(id,
- sms.getDisplayOriginatingAddress(),
- body,
- getGeckoMessageClass(sms.getMessageClass()),
- sms.getTimestampMillis(),
- timestamp);
-
- return;
- }
-
- if (intent.getAction().equals(ACTION_SMS_SENT) ||
- intent.getAction().equals(ACTION_SMS_DELIVERED)) {
- Bundle bundle = intent.getExtras();
-
- if (bundle == null || !bundle.containsKey("envelopeId") ||
- !bundle.containsKey("number") || !bundle.containsKey("message") ||
- !bundle.containsKey("requestId")) {
- Log.e("GeckoSmsManager", "Got an invalid ACTION_SMS_SENT/ACTION_SMS_DELIVERED!");
- return;
- }
-
- int envelopeId = bundle.getInt("envelopeId");
- Postman postman = Postman.getInstance();
-
- Envelope envelope = postman.getEnvelope(envelopeId);
- if (envelope == null) {
- Log.e("GeckoSmsManager", "Got an invalid envelope id (or Envelope has been destroyed)!");
- return;
- }
-
- Envelope.SubParts part = intent.getAction().equals(ACTION_SMS_SENT)
- ? Envelope.SubParts.SENT_PART
- : Envelope.SubParts.DELIVERED_PART;
- envelope.decreaseRemainingParts(part);
-
-
- if (getResultCode() != Activity.RESULT_OK) {
- switch (getResultCode()) {
- case SmsManager.RESULT_ERROR_NULL_PDU:
- envelope.setError(kInternalError);
- break;
- case SmsManager.RESULT_ERROR_NO_SERVICE:
- case SmsManager.RESULT_ERROR_RADIO_OFF:
- envelope.setError(kNoSignalError);
- break;
- case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
- default:
- envelope.setError(kUnknownError);
- break;
- }
- envelope.markAsFailed(part);
- Log.i("GeckoSmsManager", "SMS part sending failed!");
- }
-
- if (envelope.arePartsRemaining(part)) {
- return;
- }
-
- if (envelope.isFailing(part)) {
- if (part == Envelope.SubParts.SENT_PART) {
- if (bundle.getBoolean("shouldNotify")) {
- notifySmsSendFailed(envelope.getError(), bundle.getInt("requestId"));
- }
- Log.i("GeckoSmsManager", "SMS sending failed!");
- } else {
- notifySmsDelivery(envelope.getMessageId(),
- kDeliveryStatusError,
- bundle.getString("number"),
- bundle.getString("message"),
- envelope.getMessageTimestamp());
- Log.i("GeckoSmsManager", "SMS delivery failed!");
- }
- } else {
- if (part == Envelope.SubParts.SENT_PART) {
- String number = bundle.getString("number");
- String message = bundle.getString("message");
- long timestamp = System.currentTimeMillis();
-
- // save message only if we're default SMS app, otherwise sendTextMessage does it for us
- int id = 0;
- if (isDefaultSmsApp(GeckoAppShell.getContext())) {
- id = saveSentMessage(number, message, timestamp);
- }
-
- if (bundle.getBoolean("shouldNotify")) {
- notifySmsSent(id, number, message, timestamp, bundle.getInt("requestId"));
- }
-
- envelope.setMessageId(id);
- envelope.setMessageTimestamp(timestamp);
-
- Log.i("GeckoSmsManager", "SMS sending was successful!");
- } else {
- Uri id = ContentUris.withAppendedId(kSmsContentUri, envelope.getMessageId());
- updateMessageStatus(id, Telephony.Sms.STATUS_COMPLETE);
-
- notifySmsDelivery(envelope.getMessageId(),
- kDeliveryStatusSuccess,
- bundle.getString("number"),
- bundle.getString("message"),
- envelope.getMessageTimestamp());
- Log.i("GeckoSmsManager", "SMS successfully delivered!");
- }
- }
-
- // Destroy the envelope object only if the SMS has been sent and delivered.
- if (!envelope.arePartsRemaining(Envelope.SubParts.SENT_PART) &&
- !envelope.arePartsRemaining(Envelope.SubParts.DELIVERED_PART)) {
- postman.destroyEnvelope(envelopeId);
- }
- }
- }
-
- @Override
- public void send(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify) {
- int envelopeId = Postman.kUnknownEnvelopeId;
-
- try {
- SmsManager sm = SmsManager.getDefault();
-
- Intent sentIntent = new Intent(GeckoAppShell.getContext(), GeckoSmsManager.class);
- sentIntent.setAction(ACTION_SMS_SENT);
- Intent deliveredIntent = new Intent(GeckoAppShell.getContext(), GeckoSmsManager.class);
- deliveredIntent.setAction(ACTION_SMS_DELIVERED);
-
- Bundle bundle = new Bundle();
- bundle.putString("number", aNumber);
- bundle.putString("message", aMessage);
- bundle.putInt("requestId", aRequestId);
- bundle.putBoolean("shouldNotify", aShouldNotify);
-
- if (aMessage.length() <= kMaxMessageSize) {
- envelopeId = Postman.getInstance().createEnvelope(1);
- bundle.putInt("envelopeId", envelopeId);
-
- sentIntent.putExtras(bundle);
- deliveredIntent.putExtras(bundle);
-
- /*
- * There are a few things to know about getBroadcast and pending intents:
- * - the pending intents are in a shared pool maintained by the system;
- * - each pending intent is identified by a token;
- * - when a new pending intent is created, if it has the same token as
- * another intent in the pool, one of them has to be removed.
- *
- * To prevent having a hard time because of this situation, we give a
- * unique id to all pending intents we are creating. This unique id is
- * generated by GetPendingIntentUID().
- */
- PendingIntent sentPendingIntent =
- PendingIntent.getBroadcast(GeckoAppShell.getContext(),
- pendingIntentGuid.incrementAndGet(), sentIntent,
- PendingIntent.FLAG_CANCEL_CURRENT);
-
- PendingIntent deliveredPendingIntent =
- PendingIntent.getBroadcast(GeckoAppShell.getContext(),
- pendingIntentGuid.incrementAndGet(), deliveredIntent,
- PendingIntent.FLAG_CANCEL_CURRENT);
-
- sm.sendTextMessage(aNumber, null, aMessage,
- sentPendingIntent, deliveredPendingIntent);
- } else {
- ArrayList<String> parts = sm.divideMessage(aMessage);
- envelopeId = Postman.getInstance().createEnvelope(parts.size());
- bundle.putInt("envelopeId", envelopeId);
-
- sentIntent.putExtras(bundle);
- deliveredIntent.putExtras(bundle);
-
- ArrayList<PendingIntent> sentPendingIntents =
- new ArrayList<PendingIntent>(parts.size());
- ArrayList<PendingIntent> deliveredPendingIntents =
- new ArrayList<PendingIntent>(parts.size());
-
- for (int i = 0; i < parts.size(); ++i) {
- sentPendingIntents.add(
- PendingIntent.getBroadcast(GeckoAppShell.getContext(),
- pendingIntentGuid.incrementAndGet(), sentIntent,
- PendingIntent.FLAG_CANCEL_CURRENT)
- );
-
- deliveredPendingIntents.add(
- PendingIntent.getBroadcast(GeckoAppShell.getContext(),
- pendingIntentGuid.incrementAndGet(), deliveredIntent,
- PendingIntent.FLAG_CANCEL_CURRENT)
- );
- }
-
- sm.sendMultipartTextMessage(aNumber, null, parts, sentPendingIntents,
- deliveredPendingIntents);
- }
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Failed to send an SMS: ", e);
-
- if (envelopeId != Postman.kUnknownEnvelopeId) {
- Postman.getInstance().destroyEnvelope(envelopeId);
- }
-
- notifySmsSendFailed(kUnknownError, aRequestId);
- }
- }
-
- public int saveSentMessage(String aRecipient, String aBody, long aDate) {
- try {
- ContentValues values = new ContentValues();
- values.put(Telephony.Sms.ADDRESS, aRecipient);
- values.put(Telephony.Sms.BODY, aBody);
- values.put(Telephony.Sms.DATE, aDate);
- // Always 'PENDING' because we always request status report.
- values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_PENDING);
- values.put(Telephony.Sms.SEEN, 0);
- values.put(Telephony.Sms.READ, 0);
-
- ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
- Uri uri = cr.insert(kSmsSentContentUri, values);
-
- long id = ContentUris.parseId(uri);
-
- // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
- // we happen to need more than that but it doesn't cost to check.
- if (id > UNSIGNED_INTEGER_MAX_VALUE) {
- throw new IdTooHighException();
- }
-
- return (int)id;
- } catch (IdTooHighException e) {
- Log.e("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
- return -1;
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Something went wrong when trying to write a sent message", e);
- return -1;
- }
- }
-
- public void updateMessageStatus(Uri aId, int aStatus) {
- ContentValues values = new ContentValues(1);
- values.put(Telephony.Sms.STATUS, aStatus);
-
- ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
- if (cr.update(aId, values, null, null) != 1) {
- Log.i("GeckoSmsManager", "Failed to update message status!");
- }
- }
-
- public int saveReceivedMessage(Context aContext, String aSender, String aBody, long aDateSent, long aDate, String aSubject) {
- ContentValues values = new ContentValues();
- values.put(Telephony.Sms.Inbox.ADDRESS, aSender);
- values.put(Telephony.Sms.Inbox.BODY, aBody);
- values.put(Telephony.Sms.Inbox.DATE_SENT, aDateSent);
- values.put(Telephony.Sms.Inbox.DATE, aDate);
- values.put(Telephony.Sms.Inbox.STATUS, Telephony.Sms.STATUS_COMPLETE);
- values.put(Telephony.Sms.Inbox.READ, 0);
- values.put(Telephony.Sms.Inbox.SEEN, 0);
-
- ContentResolver cr = aContext.getContentResolver();
- Uri uri = cr.insert(kSmsContentUri, values);
-
- long id = ContentUris.parseId(uri);
-
- // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
- // we happen to need more than that but it doesn't cost to check.
- if (id > UNSIGNED_INTEGER_MAX_VALUE) {
- Log.i("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
- return -1;
- }
-
- return (int)id;
- }
-
- @Override
- public void getMessage(int aMessageId, int aRequestId) {
- class GetMessageRunnable implements Runnable {
- private final int mMessageId;
- private final int mRequestId;
-
- GetMessageRunnable(int aMessageId, int aRequestId) {
- mMessageId = aMessageId;
- mRequestId = aRequestId;
- }
-
- @Override
- public void run() {
- Cursor cursor = null;
-
- try {
- ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
- Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
-
- cursor = cr.query(message, kRequiredMessageRows, null, null, null);
- if (cursor == null || cursor.getCount() == 0) {
- throw new NotFoundException();
- }
-
- if (cursor.getCount() != 1) {
- throw new TooManyResultsException();
- }
-
- cursor.moveToFirst();
-
- if (cursor.getInt(cursor.getColumnIndex("_id")) != mMessageId) {
- throw new UnmatchingIdException();
- }
-
- int type = cursor.getInt(cursor.getColumnIndex("type"));
- int deliveryStatus;
- String sender = "";
- String receiver = "";
-
- if (type == kSmsTypeInbox) {
- deliveryStatus = kDeliveryStatusSuccess;
- sender = cursor.getString(cursor.getColumnIndex("address"));
- } else if (type == kSmsTypeSentbox) {
- deliveryStatus = getGeckoDeliveryStatus(cursor.getInt(cursor.getColumnIndex("status")));
- receiver = cursor.getString(cursor.getColumnIndex("address"));
- } else {
- throw new InvalidTypeException();
- }
-
- boolean read = cursor.getInt(cursor.getColumnIndex("read")) != 0;
-
- notifyGetSms(cursor.getInt(cursor.getColumnIndex("_id")),
- deliveryStatus,
- receiver, sender,
- cursor.getString(cursor.getColumnIndex("body")),
- cursor.getLong(cursor.getColumnIndex("date")),
- read,
- mRequestId);
- } catch (NotFoundException e) {
- Log.i("GeckoSmsManager", "Message id " + mMessageId + " not found");
- notifyGetSmsFailed(kNotFoundError, mRequestId);
- } catch (UnmatchingIdException e) {
- Log.e("GeckoSmsManager", "Requested message id (" + mMessageId +
- ") is different from the one we got.");
- notifyGetSmsFailed(kUnknownError, mRequestId);
- } catch (TooManyResultsException e) {
- Log.e("GeckoSmsManager", "Get too many results for id " + mMessageId);
- notifyGetSmsFailed(kUnknownError, mRequestId);
- } catch (InvalidTypeException e) {
- Log.i("GeckoSmsManager", "Message has an invalid type, we ignore it.");
- notifyGetSmsFailed(kNotFoundError, mRequestId);
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Error while trying to get message", e);
- notifyGetSmsFailed(kUnknownError, mRequestId);
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- }
-
- if (!SmsIOThread.getInstance().execute(new GetMessageRunnable(aMessageId, aRequestId))) {
- Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
- notifyGetSmsFailed(kUnknownError, aRequestId);
- }
- }
-
- @Override
- public void deleteMessage(int aMessageId, int aRequestId) {
- class DeleteMessageRunnable implements Runnable {
- private final int mMessageId;
- private final int mRequestId;
-
- DeleteMessageRunnable(int aMessageId, int aRequestId) {
- mMessageId = aMessageId;
- mRequestId = aRequestId;
- }
-
- @Override
- public void run() {
- try {
- ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
- Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
-
- int count = cr.delete(message, null, null);
-
- if (count > 1) {
- throw new TooManyResultsException();
- }
-
- notifySmsDeleted(count == 1, mRequestId);
- } catch (TooManyResultsException e) {
- Log.e("GeckoSmsManager", "Delete more than one message?", e);
- notifySmsDeleteFailed(kUnknownError, mRequestId);
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Error while trying to delete a message", e);
- notifySmsDeleteFailed(kUnknownError, mRequestId);
- }
- }
- }
-
- if (!SmsIOThread.getInstance().execute(new DeleteMessageRunnable(aMessageId, aRequestId))) {
- Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
- notifySmsDeleteFailed(kUnknownError, aRequestId);
- }
- }
-
- private void getMessageFromCursorAndNotify(Cursor aCursor, int aRequestId) throws Exception {
- int type = aCursor.getInt(aCursor.getColumnIndex("type"));
- int deliveryStatus = kDeliveryStateUnknown;
- String sender = "";
- String receiver = "";
-
- if (type == kSmsTypeInbox) {
- deliveryStatus = kDeliveryStatusSuccess;
- sender = aCursor.getString(aCursor.getColumnIndex("address"));
- } else if (type == kSmsTypeSentbox) {
- deliveryStatus = getGeckoDeliveryStatus(aCursor.getInt(aCursor.getColumnIndex("status")));
- receiver = aCursor.getString(aCursor.getColumnIndex("address"));
- } else {
- throw new Exception("Shouldn't ever get a message here that's not from inbox or sentbox");
- }
-
- boolean read = aCursor.getInt(aCursor.getColumnIndex("read")) != 0;
-
- notifyMessageCursorResult(aCursor.getInt(aCursor.getColumnIndex("_id")),
- deliveryStatus,
- receiver, sender,
- aCursor.getString(aCursor.getColumnIndex("body")),
- aCursor.getLong(aCursor.getColumnIndex("date")),
- aCursor.getLong(aCursor.getColumnIndex("thread_id")),
- read,
- aRequestId);
- }
-
- @Override
- public void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId) {
- class MarkMessageReadRunnable implements Runnable {
- private final int mMessageId;
- private final boolean mValue;
- private final int mRequestId;
-
- MarkMessageReadRunnable(int aMessageId, boolean aValue, int aRequestId) {
- mMessageId = aMessageId;
- mValue = aValue;
- mRequestId = aRequestId;
- }
-
- @Override
- public void run() {
- try {
- ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
- Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
-
- ContentValues updatedProps = new ContentValues();
- updatedProps.put("read", mValue);
-
- int count = cr.update(message, updatedProps, null, null);
-
- notifySmsMarkedAsRead(count == 1, mRequestId);
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Error while trying to mark message as read: " + e);
- notifySmsMarkAsReadFailed(kUnknownError, mRequestId);
- }
- }
- }
-
- if (aSendReadReport == true) {
- Log.w("GeckoSmsManager", "Android SmsManager doesn't suport read receipts for SMS.");
- }
-
- if (!SmsIOThread.getInstance().execute(new MarkMessageReadRunnable(aMessageId, aValue, aRequestId))) {
- Log.e("GeckoSmsManager", "Failed to add MarkMessageReadRunnable to the SmsIOThread");
- notifySmsMarkAsReadFailed(kUnknownError, aRequestId);
- }
- }
-
- @Override
- public void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
- class CreateMessageCursorRunnable implements Runnable {
- private final long mStartDate;
- private final long mEndDate;
- private final String[] mNumbers;
- private final int mNumbersCount;
- private final String mDelivery;
- private final boolean mHasThreadId;
- private final long mThreadId;
- private final boolean mReverse;
- private final int mRequestId;
-
- CreateMessageCursorRunnable(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
- mStartDate = aStartDate;
- mEndDate = aEndDate;
- mNumbers = aNumbers;
- mNumbersCount = aNumbersCount;
- mDelivery = aDelivery;
- mHasThreadId = aHasThreadId;
- mThreadId = aThreadId;
- mReverse = aReverse;
- mRequestId = aRequestId;
- }
-
- @Override
- public void run() {
- Cursor cursor = null;
- boolean closeCursor = true;
-
- try {
- StringBuilder restrictions = new StringBuilder();
- Formatter formatter = new Formatter(restrictions);
-
- if (mStartDate >= 0) {
- formatter.format("date >= '%d' AND ", mStartDate);
- }
-
- if (mEndDate >= 0) {
- formatter.format("date <= '%d' AND ", mEndDate);
- }
-
- if (mNumbersCount > 0) {
- formatter.format("address IN ('%s'", mNumbers[0]);
-
- for (int i = 1; i < mNumbersCount; ++i) {
- formatter.format(", '%s'", mNumbers[i]);
- }
-
- formatter.format(") AND ");
- }
-
- if (mDelivery == null || mDelivery.isEmpty()) {
- formatter.format("type IN ('%d', '%d') AND ", kSmsTypeSentbox, kSmsTypeInbox);
- } else if (mDelivery.equals("sent")) {
- formatter.format("type = '%d' AND ", kSmsTypeSentbox);
- } else if (mDelivery.equals("received")) {
- formatter.format("type = '%d' AND ", kSmsTypeInbox);
- } else {
- throw new Exception("Unexpected delivery state: " + mDelivery);
- }
-
- if (mHasThreadId) {
- formatter.format("thread_id = '%d' AND ", mThreadId);
- }
-
- // Final 'AND 1' is a no-op so we don't have to special case the last
- // condition.
- formatter.format("1");
-
- ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
- cursor = cr.query(kSmsContentUri,
- kRequiredMessageRows,
- restrictions.toString(),
- null,
- mReverse ? "date DESC" : "date ASC");
-
- if (cursor.getCount() == 0) {
- notifyCursorDone(mRequestId);
- return;
- }
-
- MessagesListManager.getInstance().add(mRequestId, cursor);
-
- cursor.moveToFirst();
- getMessageFromCursorAndNotify(cursor, mRequestId);
- closeCursor = false;
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Error while trying to create a message list cursor", e);
- notifyCursorError(kUnknownError, mRequestId);
- } finally {
- if (closeCursor && cursor != null) {
- cursor.close();
- }
- }
- }
- }
-
- if (!SmsIOThread.getInstance().execute(new CreateMessageCursorRunnable(aStartDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, aThreadId, aReverse, aRequestId))) {
- Log.e("GeckoSmsManager", "Failed to add CreateMessageCursorRunnable to the SmsIOThread");
- notifyCursorError(kUnknownError, aRequestId);
- }
- }
-
- @Override
- public void getNextMessage(int aRequestId) {
- class GetNextMessageRunnable implements Runnable {
- private final int mRequestId;
-
- GetNextMessageRunnable(int aRequestId) {
- mRequestId = aRequestId;
- }
-
- @Override
- public void run() {
- Cursor cursor = null;
- boolean closeCursor = true;
- try {
- cursor = MessagesListManager.getInstance().get(mRequestId);
-
- if (!cursor.moveToNext()) {
- MessagesListManager.getInstance().remove(mRequestId);
- notifyCursorDone(mRequestId);
- return;
- }
-
- getMessageFromCursorAndNotify(cursor, mRequestId);
- closeCursor = false;
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Error while trying to get the next message of a list", e);
- notifyCursorError(kUnknownError, mRequestId);
- } finally {
- if (closeCursor) {
- cursor.close();
- }
- }
- }
- }
-
- if (!SmsIOThread.getInstance().execute(new GetNextMessageRunnable(aRequestId))) {
- Log.e("GeckoSmsManager", "Failed to add GetNextMessageRunnable to the SmsIOThread");
- notifyCursorError(kUnknownError, aRequestId);
- }
- }
-
- private void getThreadFromCursorAndNotify(Cursor aCursor, int aRequestId) throws Exception {
- ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-
- long id = aCursor.getLong(aCursor.getColumnIndex("thread_id"));
- Cursor msgCursor = cr.query(kSmsContentUri,
- kRequiredMessageRowsForThread,
- "thread_id = " + id,
- null,
- "date DESC");
-
- if (msgCursor == null || msgCursor.getCount() == 0) {
- throw new Exception("Empty thread " + id);
- }
-
- msgCursor.moveToFirst();
-
- String lastMessageSubject = msgCursor.getString(msgCursor.getColumnIndex("subject"));
- String body = msgCursor.getString(msgCursor.getColumnIndex("body"));
- long timestamp = msgCursor.getLong(msgCursor.getColumnIndex("date"));
-
- HashSet<String> participants = new HashSet<>();
- do {
- String p = msgCursor.getString(msgCursor.getColumnIndex("address"));
- participants.add(p);
- } while (msgCursor.moveToNext());
-
- //TODO: handle MMS
- String lastMessageType = "sms";
-
- msgCursor = cr.query(kSmsContentUri,
- kRequiredMessageRowsForThread,
- "thread_id = " + id + " AND read = 0",
- null,
- null);
-
- if (msgCursor == null) {
- Log.e("GeckoSmsManager", "We should never get here, should have errored before");
- throw new Exception("Empty thread " + id);
- }
-
- long unreadCount = msgCursor.getCount();
-
- notifyThreadCursorResult(id, lastMessageSubject, body, unreadCount, participants.toArray(), timestamp, lastMessageType, aRequestId);
- }
-
- @Override
- public void createThreadCursor(int aRequestId) {
- class CreateThreadCursorRunnable implements Runnable {
- private final int mRequestId;
-
- CreateThreadCursorRunnable(int aRequestId) {
- mRequestId = aRequestId;
- }
-
- @Override
- public void run() {
- try {
- ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
- Cursor cursor = cr.query(kSmsThreadsContentUri,
- kThreadProjection,
- null,
- null,
- "date DESC");
- if (cursor == null || !cursor.moveToFirst()) {
- notifyCursorDone(mRequestId);
- return;
- }
-
- MessagesListManager.getInstance().add(mRequestId, cursor);
-
- getThreadFromCursorAndNotify(cursor, mRequestId);
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Error while trying to create thread cursor: " + e);
- notifyCursorError(kUnknownError, mRequestId);
- }
- }
- }
-
- if (!SmsIOThread.getInstance().execute(new CreateThreadCursorRunnable(aRequestId))) {
- Log.e("GeckoSmsManager", "Failed to add CreateThreadCursorRunnable to the SmsIOThread");
- notifyCursorError(kUnknownError, aRequestId);
- }
- }
-
- @Override
- public void getNextThread(int aRequestId) {
- class GetNextThreadRunnable implements Runnable {
- private final int mRequestId;
-
- GetNextThreadRunnable(int aRequestId) {
- mRequestId = aRequestId;
- }
-
- @Override
- public void run() {
- try {
- Cursor cursor = MessagesListManager.getInstance().get(mRequestId);
-
- if (!cursor.moveToNext()) {
- MessagesListManager.getInstance().remove(mRequestId);
- notifyCursorDone(mRequestId);
- return;
- }
-
- getThreadFromCursorAndNotify(cursor, mRequestId);
- } catch (Exception e) {
- Log.e("GeckoSmsManager", "Error while trying to create thread cursor: " + e);
- notifyCursorError(kUnknownError, mRequestId);
- }
- }
- }
-
- if (!SmsIOThread.getInstance().execute(new GetNextThreadRunnable(aRequestId))) {
- Log.e("GeckoSmsManager", "Failed to add GetNextThreadRunnable to the SmsIOThread");
- notifyCursorError(kUnknownError, aRequestId);
- }
- }
-
- @Override
- public void stop() {
- GeckoAppShell.getContext().unregisterReceiver(this);
- }
-
- @Override
- public void shutdown() {
- SmsIOThread.getInstance().interrupt();
- MessagesListManager.getInstance().clear();
- }
-
- private int getGeckoDeliveryStatus(int aDeliveryStatus) {
- if (aDeliveryStatus == Telephony.Sms.STATUS_NONE) {
- return kDeliveryStatusNotApplicable;
- }
- if (aDeliveryStatus >= Telephony.Sms.STATUS_FAILED) {
- return kDeliveryStatusError;
- }
- if (aDeliveryStatus >= Telephony.Sms.STATUS_PENDING) {
- return kDeliveryStatusPending;
- }
- return kDeliveryStatusSuccess;
- }
-
- private int getGeckoMessageClass(MessageClass aMessageClass) {
- switch (aMessageClass) {
- case CLASS_0:
- return kMessageClassClass0;
- case CLASS_1:
- return kMessageClassClass1;
- case CLASS_2:
- return kMessageClassClass2;
- case CLASS_3:
- return kMessageClassClass3;
- default:
- return kMessageClassNormal;
- }
- }
-
- static class IdTooHighException extends Exception {
- private static final long serialVersionUID = 29935575131092050L;
- }
-
- static class InvalidTypeException extends Exception {
- private static final long serialVersionUID = 47436856832535912L;
- }
-
- static class NotFoundException extends Exception {
- private static final long serialVersionUID = 1940676816633984L;
- }
-
- static class TooManyResultsException extends Exception {
- private static final long serialVersionUID = 51883196784325305L;
- }
-
- static class UnmatchingIdException extends Exception {
- private static final long serialVersionUID = 158467542575633280L;
- }
-
- @WrapForJNI
- public static native void notifySmsReceived(int aId, String aSender, String aBody, int aMessageClass, long aSentTimestamp, long aTimestamp);
- @WrapForJNI
- private static native void notifySmsSent(int aId, String aReceiver, String aBody, long aTimestamp, int aRequestId);
- @WrapForJNI
- private static native void notifySmsDelivery(int aId, int aDeliveryStatus, String aReceiver, String aBody, long aTimestamp);
- @WrapForJNI
- private static native void notifySmsSendFailed(int aError, int aRequestId);
- @WrapForJNI
- private static native void notifyGetSms(int aId, int aDeliveryStatus, String aReceiver, String aSender, String aBody, long aTimestamp, boolean aRead, int aRequestId);
- @WrapForJNI
- private static native void notifyGetSmsFailed(int aError, int aRequestId);
- @WrapForJNI
- private static native void notifySmsDeleted(boolean aDeleted, int aRequestId);
- @WrapForJNI
- private static native void notifySmsDeleteFailed(int aError, int aRequestId);
- @WrapForJNI
- private static native void notifySmsMarkedAsRead(boolean aMarkedAsRead, int aRequestId);
- @WrapForJNI
- private static native void notifySmsMarkAsReadFailed(int aError, int aRequestId);
- @WrapForJNI
- private static native void notifyCursorError(int aError, int aRequestId);
- @WrapForJNI
- private static native void notifyThreadCursorResult(long aId, String aLastMessageSubject, String aBody, long aUnreadCount, Object[] aParticipants, long aTimestamp, String aLastMessageType, int aRequestId);
- @WrapForJNI
- private static native void notifyMessageCursorResult(int aMessageId, int aDeliveryStatus, String aReceiver, String aSender, String aBody, long aTimestamp, long aThreadId, boolean aRead, int aRequestId);
- @WrapForJNI
- private static native void notifyCursorDone(int aRequestId);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoThread.java
+++ /dev/null
@@ -1,680 +0,0 @@
-/* -*- 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.annotation.WrapForJNI;
-import org.mozilla.gecko.mozglue.GeckoLoader;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.MessageQueue;
-import android.os.SystemClock;
-import android.util.Log;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.Locale;
-
-public class GeckoThread extends Thread {
- private static final String LOGTAG = "GeckoThread";
-
- public enum State {
- // After being loaded by class loader.
- @WrapForJNI INITIAL(0),
- // After launching Gecko thread
- @WrapForJNI LAUNCHED(1),
- // After loading the mozglue library.
- @WrapForJNI MOZGLUE_READY(2),
- // After loading the libxul library.
- @WrapForJNI LIBS_READY(3),
- // After initializing nsAppShell and JNI calls.
- @WrapForJNI JNI_READY(4),
- // After initializing profile and prefs.
- @WrapForJNI PROFILE_READY(5),
- // After initializing frontend JS
- @WrapForJNI RUNNING(6),
- // After leaving Gecko event loop
- @WrapForJNI EXITING(3),
- // After exiting GeckoThread (corresponding to "Gecko:Exited" event)
- @WrapForJNI EXITED(0);
-
- /* The rank is an arbitrary value reflecting the amount of components or features
- * that are available for use. During startup and up to the RUNNING state, the
- * rank value increases because more components are initialized and available for
- * use. During shutdown and up to the EXITED state, the rank value decreases as
- * components are shut down and become unavailable. EXITING has the same rank as
- * LIBS_READY because both states have a similar amount of components available.
- */
- private final int rank;
-
- private State(int rank) {
- this.rank = rank;
- }
-
- public boolean is(final State other) {
- return this == other;
- }
-
- public boolean isAtLeast(final State other) {
- return this.rank >= other.rank;
- }
-
- public boolean isAtMost(final State other) {
- return this.rank <= other.rank;
- }
-
- // Inclusive
- public boolean isBetween(final State min, final State max) {
- return this.rank >= min.rank && this.rank <= max.rank;
- }
- }
-
- public static final State MIN_STATE = State.INITIAL;
- public static final State MAX_STATE = State.EXITED;
-
- private static volatile State sState = State.INITIAL;
-
- private static class QueuedCall {
- public Method method;
- public Object target;
- public Object[] args;
- public State state;
-
- public QueuedCall(final Method method, final Object target,
- final Object[] args, final State state) {
- this.method = method;
- this.target = target;
- this.args = args;
- this.state = state;
- }
- }
-
- private static final int QUEUED_CALLS_COUNT = 16;
- private static final ArrayList<QueuedCall> QUEUED_CALLS = new ArrayList<>(QUEUED_CALLS_COUNT);
-
- private static GeckoThread sGeckoThread;
-
- @WrapForJNI
- private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader();
- @WrapForJNI
- private static MessageQueue msgQueue;
-
- private GeckoProfile mProfile;
-
- private final String mArgs;
- private final String mAction;
- private final boolean mDebugging;
-
- GeckoThread(GeckoProfile profile, String args, String action, boolean debugging) {
- mProfile = profile;
- mArgs = args;
- mAction = action;
- mDebugging = debugging;
-
- setName("Gecko");
- }
-
- public static boolean init(GeckoProfile profile, String args, String action, boolean debugging) {
- ThreadUtils.assertOnUiThread();
- if (isState(State.INITIAL) && sGeckoThread == null) {
- sGeckoThread = new GeckoThread(profile, args, action, debugging);
- return true;
- }
- return false;
- }
-
- private static boolean canUseProfile(final Context context, final GeckoProfile profile,
- final String profileName, final File profileDir) {
- if (profileDir != null && !profileDir.isDirectory()) {
- return false;
- }
-
- if (profile == null) {
- // We haven't initialized; any profile is okay as long as we follow the guest mode setting.
- return GeckoProfile.shouldUse(context) ==
- GeckoProfile.isGuestProfile(context, profileName, profileDir);
- }
-
- // We already initialized and have a profile; see if it matches ours.
- try {
- return profileDir == null ? profileName.equals(profile.getName()) :
- profile.getDir().getCanonicalPath().equals(profileDir.getCanonicalPath());
- } catch (final IOException e) {
- Log.e(LOGTAG, "Cannot compare profile " + profileName);
- return false;
- }
- }
-
- public static boolean canUseProfile(final String profileName, final File profileDir) {
- if (profileName == null) {
- throw new IllegalArgumentException("Null profile name");
- }
- return canUseProfile(GeckoAppShell.getApplicationContext(), getActiveProfile(),
- profileName, profileDir);
- }
-
- public static boolean initWithProfile(final String profileName, final File profileDir) {
- if (profileName == null) {
- throw new IllegalArgumentException("Null profile name");
- }
-
- final Context context = GeckoAppShell.getApplicationContext();
- final GeckoProfile profile = getActiveProfile();
-
- if (!canUseProfile(context, profile, profileName, profileDir)) {
- // Profile is incompatible with current profile.
- return false;
- }
-
- if (profile != null) {
- // We already have a compatible profile.
- return true;
- }
-
- // We haven't initialized yet; okay to initialize now.
- return init(GeckoProfile.get(context, profileName, profileDir),
- /* args */ null, /* action */ null, /* debugging */ false);
- }
-
- public static boolean launch() {
- ThreadUtils.assertOnUiThread();
- if (checkAndSetState(State.INITIAL, State.LAUNCHED)) {
- sGeckoThread.start();
- return true;
- }
- return false;
- }
-
- public static boolean isLaunched() {
- return !isState(State.INITIAL);
- }
-
- @RobocopTarget
- public static boolean isRunning() {
- return isState(State.RUNNING);
- }
-
- // Invoke the given Method and handle checked Exceptions.
- private static void invokeMethod(final Method method, final Object obj, final Object[] args) {
- try {
- method.setAccessible(true);
- method.invoke(obj, args);
- } catch (final IllegalAccessException e) {
- throw new IllegalStateException("Unexpected exception", e);
- } catch (final InvocationTargetException e) {
- throw new UnsupportedOperationException("Cannot make call", e.getCause());
- }
- }
-
- // Queue a call to the given method.
- private static void queueNativeCallLocked(final Class<?> cls, final String methodName,
- final Object obj, final Object[] args,
- final State state) {
- final ArrayList<Class<?>> argTypes = new ArrayList<>(args.length);
- final ArrayList<Object> argValues = new ArrayList<>(args.length);
-
- for (int i = 0; i < args.length; i++) {
- if (args[i] instanceof Class) {
- argTypes.add((Class<?>) args[i]);
- argValues.add(args[++i]);
- continue;
- }
- Class<?> argType = args[i].getClass();
- if (argType == Boolean.class) argType = Boolean.TYPE;
- else if (argType == Byte.class) argType = Byte.TYPE;
- else if (argType == Character.class) argType = Character.TYPE;
- else if (argType == Double.class) argType = Double.TYPE;
- else if (argType == Float.class) argType = Float.TYPE;
- else if (argType == Integer.class) argType = Integer.TYPE;
- else if (argType == Long.class) argType = Long.TYPE;
- else if (argType == Short.class) argType = Short.TYPE;
- argTypes.add(argType);
- argValues.add(args[i]);
- }
- final Method method;
- try {
- method = cls.getDeclaredMethod(
- methodName, argTypes.toArray(new Class<?>[argTypes.size()]));
- } catch (final NoSuchMethodException e) {
- throw new IllegalArgumentException("Cannot find method", e);
- }
-
- if (!Modifier.isNative(method.getModifiers())) {
- // As a precaution, we disallow queuing non-native methods. Queuing non-native
- // methods is dangerous because the method could end up being called on either
- // the original thread or the Gecko thread depending on timing. Native methods
- // usually handle this by posting an event to the Gecko thread automatically,
- // but there is no automatic mechanism for non-native methods.
- throw new UnsupportedOperationException("Not allowed to queue non-native methods");
- }
-
- if (isStateAtLeast(state)) {
- invokeMethod(method, obj, argValues.toArray());
- return;
- }
-
- QUEUED_CALLS.add(new QueuedCall(
- method, obj, argValues.toArray(), state));
- }
-
- /**
- * Queue a call to the given static method until Gecko is in the given state.
- *
- * @param state The Gecko state in which the native call could be executed.
- * Default is State.RUNNING, which means this queued call will
- * run when Gecko is at or after RUNNING state.
- * @param cls Class that declares the static method.
- * @param methodName Name of the static method.
- * @param args Args to call the static method with; to specify a parameter type,
- * pass in a Class instance first, followed by the value.
- */
- public static void queueNativeCallUntil(final State state, final Class<?> cls,
- final String methodName, final Object... args) {
- synchronized (QUEUED_CALLS) {
- queueNativeCallLocked(cls, methodName, null, args, state);
- }
- }
-
- /**
- * Queue a call to the given static method until Gecko is in the RUNNING state.
- */
- public static void queueNativeCall(final Class<?> cls, final String methodName,
- final Object... args) {
- synchronized (QUEUED_CALLS) {
- queueNativeCallLocked(cls, methodName, null, args, State.RUNNING);
- }
- }
-
- /**
- * Queue a call to the given instance method until Gecko is in the given state.
- *
- * @param state The Gecko state in which the native call could be executed.
- * @param obj Object that declares the instance method.
- * @param methodName Name of the instance method.
- * @param args Args to call the instance method with; to specify a parameter type,
- * pass in a Class instance first, followed by the value.
- */
- public static void queueNativeCallUntil(final State state, final Object obj,
- final String methodName, final Object... args) {
- synchronized (QUEUED_CALLS) {
- queueNativeCallLocked(obj.getClass(), methodName, obj, args, state);
- }
- }
-
- /**
- * Queue a call to the given instance method until Gecko is in the RUNNING state.
- */
- public static void queueNativeCall(final Object obj, final String methodName,
- final Object... args) {
- synchronized (QUEUED_CALLS) {
- queueNativeCallLocked(obj.getClass(), methodName, obj, args, State.RUNNING);
- }
- }
-
- // Run all queued methods
- private static void flushQueuedNativeCallsLocked(final State state) {
- int lastSkipped = -1;
- for (int i = 0; i < QUEUED_CALLS.size(); i++) {
- final QueuedCall call = QUEUED_CALLS.get(i);
- if (call == null) {
- // We already handled the call.
- continue;
- }
- if (!state.isAtLeast(call.state)) {
- // The call is not ready yet; skip it.
- lastSkipped = i;
- continue;
- }
- // Mark as handled.
- QUEUED_CALLS.set(i, null);
-
- if (call.method == null) {
- final GeckoEvent e = (GeckoEvent) call.target;
- GeckoAppShell.notifyGeckoOfEvent(e);
- e.recycle();
- continue;
- }
- invokeMethod(call.method, call.target, call.args);
- }
- if (lastSkipped < 0) {
- // We're done here; release the memory
- QUEUED_CALLS.clear();
- QUEUED_CALLS.trimToSize();
- } else if (lastSkipped < QUEUED_CALLS.size() - 1) {
- // We skipped some; free up null entries at the end,
- // but keep all the previous entries for later.
- QUEUED_CALLS.subList(lastSkipped + 1, QUEUED_CALLS.size()).clear();
- }
- }
-
- private static String initGeckoEnvironment() {
- final Context context = GeckoAppShell.getApplicationContext();
- GeckoLoader.loadMozGlue(context);
- setState(State.MOZGLUE_READY);
-
- final Locale locale = Locale.getDefault();
- final Resources res = context.getResources();
- if (locale.toString().equalsIgnoreCase("zh_hk")) {
- final Locale mappedLocale = Locale.TRADITIONAL_CHINESE;
- Locale.setDefault(mappedLocale);
- Configuration config = res.getConfiguration();
- config.locale = mappedLocale;
- res.updateConfiguration(config, null);
- }
-
- String[] pluginDirs = null;
- try {
- pluginDirs = GeckoAppShell.getPluginDirectories();
- } catch (Exception e) {
- Log.w(LOGTAG, "Caught exception getting plugin dirs.", e);
- }
-
- final String resourcePath = context.getPackageResourcePath();
- GeckoLoader.setupGeckoEnvironment(context, pluginDirs, context.getFilesDir().getPath());
-
- GeckoLoader.loadSQLiteLibs(context, resourcePath);
- GeckoLoader.loadNSSLibs(context, resourcePath);
- GeckoLoader.loadGeckoLibs(context, resourcePath);
- setState(State.LIBS_READY);
-
- return resourcePath;
- }
-
- private static String getTypeFromAction(String action) {
- if (GeckoAppShell.ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
- return "-bookmark";
- }
- return null;
- }
-
- private String addCustomProfileArg(String args) {
- String profileArg = "";
-
- // Make sure a profile exists.
- final GeckoProfile profile = getProfile();
- profile.getDir(); // call the lazy initializer
-
- // If args don't include the profile, make sure it's included.
- if (args == null || !args.matches(".*\\B-(P|profile)\\s+\\S+.*")) {
- if (profile.isCustomProfile()) {
- profileArg = " -profile " + profile.getDir().getAbsolutePath();
- } else {
- profileArg = " -P " + profile.getName();
- }
- }
-
- return (args != null ? args : "") + profileArg;
- }
-
- private String getGeckoArgs(final String apkPath) {
- // argv[0] is the program name, which for us is the package name.
- final Context context = GeckoAppShell.getApplicationContext();
- final StringBuilder args = new StringBuilder(context.getPackageName());
- args.append(" -greomni ").append(apkPath);
-
- final String userArgs = addCustomProfileArg(mArgs);
- if (userArgs != null) {
- args.append(' ').append(userArgs);
- }
-
- final String type = getTypeFromAction(mAction);
- if (type != null) {
- args.append(" ").append(type);
- }
-
- // In un-official builds, we want to load Javascript resources fresh
- // with each build. In official builds, the startup cache is purged by
- // the buildid mechanism, but most un-official builds don't bump the
- // buildid, so we purge here instead.
- if (!AppConstants.MOZILLA_OFFICIAL) {
- Log.w(LOGTAG, "STARTUP PERFORMANCE WARNING: un-official build: purging the " +
- "startup (JavaScript) caches.");
- args.append(" -purgecaches");
- }
-
- return args.toString();
- }
-
- public static GeckoProfile getActiveProfile() {
- if (sGeckoThread == null) {
- return null;
- }
- final GeckoProfile profile = sGeckoThread.mProfile;
- if (profile != null) {
- return profile;
- }
- return sGeckoThread.getProfile();
- }
-
- public synchronized GeckoProfile getProfile() {
- if (mProfile == null) {
- final Context context = GeckoAppShell.getApplicationContext();
- mProfile = GeckoProfile.initFromArgs(context, mArgs);
- }
- return mProfile;
- }
-
- @Override
- public void run() {
- Looper.prepare();
- GeckoThread.msgQueue = Looper.myQueue();
- ThreadUtils.sGeckoThread = this;
- ThreadUtils.sGeckoHandler = new Handler();
-
- // Preparation for pumpMessageLoop()
- final MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
- @Override public boolean queueIdle() {
- final Handler geckoHandler = ThreadUtils.sGeckoHandler;
- Message idleMsg = Message.obtain(geckoHandler);
- // Use |Message.obj == GeckoHandler| to identify our "queue is empty" message
- idleMsg.obj = geckoHandler;
- geckoHandler.sendMessageAtFrontOfQueue(idleMsg);
- // Keep this IdleHandler
- return true;
- }
- };
- Looper.myQueue().addIdleHandler(idleHandler);
-
- if (mDebugging) {
- try {
- Thread.sleep(5 * 1000 /* 5 seconds */);
- } catch (final InterruptedException e) {
- }
- }
-
- final String args = getGeckoArgs(initGeckoEnvironment());
-
- // This can only happen after the call to initGeckoEnvironment
- // above, because otherwise the JNI code hasn't been loaded yet.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override public void run() {
- GeckoAppShell.registerJavaUiThread();
- }
- });
-
- Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - runGecko");
-
- if (!AppConstants.MOZILLA_OFFICIAL) {
- Log.i(LOGTAG, "RunGecko - args = " + args);
- }
-
- // And go.
- GeckoLoader.nativeRun(args);
-
- // And... we're done.
- setState(State.EXITED);
-
- try {
- final JSONObject msg = new JSONObject();
- msg.put("type", "Gecko:Exited");
- EventDispatcher.getInstance().dispatchEvent(msg, null);
- } catch (final JSONException e) {
- Log.e(LOGTAG, "unable to dispatch event", e);
- }
-
- // Remove pumpMessageLoop() idle handler
- Looper.myQueue().removeIdleHandler(idleHandler);
- }
-
- public static void addPendingEvent(final GeckoEvent e) {
- synchronized (QUEUED_CALLS) {
- if (isRunning()) {
- // We may just have switched to running state.
- GeckoAppShell.notifyGeckoOfEvent(e);
- e.recycle();
- } else {
- QUEUED_CALLS.add(new QueuedCall(null, e, null, State.RUNNING));
- }
- }
- }
-
- @WrapForJNI
- private static boolean pumpMessageLoop(final Message msg) {
- final Handler geckoHandler = ThreadUtils.sGeckoHandler;
-
- if (msg.obj == geckoHandler && msg.getTarget() == geckoHandler) {
- // Our "queue is empty" message; see runGecko()
- return false;
- }
-
- if (msg.getTarget() == null) {
- Looper.myLooper().quit();
- } else {
- msg.getTarget().dispatchMessage(msg);
- }
-
- return true;
- }
-
- /**
- * Check that the current Gecko thread state matches the given state.
- *
- * @param state State to check
- * @return True if the current Gecko thread state matches
- */
- public static boolean isState(final State state) {
- return sState.is(state);
- }
-
- /**
- * Check that the current Gecko thread state is at the given state or further along,
- * according to the order defined in the State enum.
- *
- * @param state State to check
- * @return True if the current Gecko thread state matches
- */
- public static boolean isStateAtLeast(final State state) {
- return sState.isAtLeast(state);
- }
-
- /**
- * Check that the current Gecko thread state is at the given state or prior,
- * according to the order defined in the State enum.
- *
- * @param state State to check
- * @return True if the current Gecko thread state matches
- */
- public static boolean isStateAtMost(final State state) {
- return sState.isAtMost(state);
- }
-
- /**
- * Check that the current Gecko thread state falls into an inclusive range of states,
- * according to the order defined in the State enum.
- *
- * @param minState Lower range of allowable states
- * @param maxState Upper range of allowable states
- * @return True if the current Gecko thread state matches
- */
- public static boolean isStateBetween(final State minState, final State maxState) {
- return sState.isBetween(minState, maxState);
- }
-
- @WrapForJNI
- private static void setState(final State newState) {
- ThreadUtils.assertOnGeckoThread();
- synchronized (QUEUED_CALLS) {
- flushQueuedNativeCallsLocked(newState);
- sState = newState;
- }
- }
-
- @WrapForJNI
- private static boolean checkAndSetState(final State currentState, final State newState) {
- synchronized (QUEUED_CALLS) {
- if (sState == currentState) {
- flushQueuedNativeCallsLocked(newState);
- sState = newState;
- return true;
- }
- }
- return false;
- }
-
- @WrapForJNI(stubName = "SpeculativeConnect")
- private static native void speculativeConnectNative(String uri);
-
- public static void speculativeConnect(final String uri) {
- // This is almost always called before Gecko loads, so we don't
- // bother checking here if Gecko is actually loaded or not.
- // Speculative connection depends on proxy settings,
- // so the earliest it can happen is after profile is ready.
- queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
- "speculativeConnectNative", uri);
- }
-
- @WrapForJNI @RobocopTarget
- public static native void waitOnGecko();
-
- @WrapForJNI(stubName = "OnPause")
- private static native void nativeOnPause();
-
- public static void onPause() {
- if (isStateAtLeast(State.PROFILE_READY)) {
- nativeOnPause();
- } else {
- queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
- "nativeOnPause");
- }
- }
-
- @WrapForJNI(stubName = "OnResume")
- private static native void nativeOnResume();
-
- public static void onResume() {
- if (isStateAtLeast(State.PROFILE_READY)) {
- nativeOnResume();
- } else {
- queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
- "nativeOnResume");
- }
- }
-
- @WrapForJNI(stubName = "CreateServices")
- private static native void nativeCreateServices(String category, String data);
-
- public static void createServices(final String category, final String data) {
- if (isStateAtLeast(State.PROFILE_READY)) {
- nativeCreateServices(category, data);
- } else {
- queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class, "nativeCreateServices",
- String.class, category, String.class, data);
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoView.java
+++ /dev/null
@@ -1,708 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.util.Set;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.ReflectionTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.gfx.GLController;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.mozglue.JNIObject;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-
-public class GeckoView extends LayerView
- implements ContextGetter, GeckoEventListener, NativeEventListener {
-
- private static final String DEFAULT_SHARED_PREFERENCES_FILE = "GeckoView";
- private static final String LOGTAG = "GeckoView";
-
- private ChromeDelegate mChromeDelegate;
- private ContentDelegate mContentDelegate;
-
- private InputConnectionListener mInputConnectionListener;
-
- @Override
- public void handleMessage(final String event, final JSONObject message) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- try {
- if (event.equals("Gecko:Ready")) {
- handleReady(message);
- } else if (event.equals("Content:StateChange")) {
- handleStateChange(message);
- } else if (event.equals("Content:LoadError")) {
- handleLoadError(message);
- } else if (event.equals("Content:PageShow")) {
- handlePageShow(message);
- } else if (event.equals("DOMTitleChanged")) {
- handleTitleChanged(message);
- } else if (event.equals("Link:Favicon")) {
- handleLinkFavicon(message);
- } else if (event.equals("Prompt:Show") || event.equals("Prompt:ShowTop")) {
- handlePrompt(message);
- } else if (event.equals("Accessibility:Event")) {
- int mode = getImportantForAccessibility();
- if (mode == View.IMPORTANT_FOR_ACCESSIBILITY_YES ||
- mode == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
- GeckoAccessibility.sendAccessibilityEvent(message);
- }
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "handleMessage threw for " + event, e);
- }
- }
- });
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
- try {
- if ("Accessibility:Ready".equals(event)) {
- GeckoAccessibility.updateAccessibilitySettings(getContext());
- } else if ("GeckoView:Message".equals(event)) {
- // We need to pull out the bundle while on the Gecko thread.
- NativeJSObject json = message.optObject("data", null);
- if (json == null) {
- // Must have payload to call the message handler.
- return;
- }
- final Bundle data = json.toBundle();
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- handleScriptMessage(data, callback);
- }
- });
- }
- } catch (Exception e) {
- Log.w(LOGTAG, "handleMessage threw for " + event, e);
- }
- }
-
- @WrapForJNI
- private static final class Window extends JNIObject {
- /* package */ final GLController glController = new GLController();
-
- static native void open(Window instance, GeckoView view, GLController glController,
- String chromeURI,
- int width, int height);
-
- @Override protected native void disposeNative();
- native void close();
- native void reattach(GeckoView view);
- }
-
- // Object to hold onto our nsWindow connection when GeckoView gets destroyed.
- private static class StateBinder extends Binder implements Parcelable {
- public final Parcelable superState;
- public final Window window;
-
- public StateBinder(Parcelable superState, Window window) {
- this.superState = superState;
- this.window = window;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- // Always write out the super-state, so that even if we lose this binder, we
- // will still have something to pass into super.onRestoreInstanceState.
- out.writeParcelable(superState, flags);
- out.writeStrongBinder(this);
- }
-
- @ReflectionTarget
- public static final Parcelable.Creator<StateBinder> CREATOR
- = new Parcelable.Creator<StateBinder>() {
- @Override
- public StateBinder createFromParcel(Parcel in) {
- final Parcelable superState = in.readParcelable(null);
- final IBinder binder = in.readStrongBinder();
- if (binder instanceof StateBinder) {
- return (StateBinder) binder;
- }
- // Not the original object we saved; return null state.
- return new StateBinder(superState, null);
- }
-
- @Override
- public StateBinder[] newArray(int size) {
- return new StateBinder[size];
- }
- };
- }
-
- private Window window;
- private boolean stateSaved;
-
- public GeckoView(Context context) {
- super(context);
- init(context);
- }
-
- public GeckoView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
-
- private void init(Context context) {
- if (GeckoAppShell.getApplicationContext() == null) {
- GeckoAppShell.setApplicationContext(context.getApplicationContext());
- }
-
- // Set the GeckoInterface if the context is an activity and the GeckoInterface
- // has not already been set
- if (context instanceof Activity && getGeckoInterface() == null) {
- setGeckoInterface(new BaseGeckoInterface(context));
- GeckoAppShell.setContextGetter(this);
- }
-
- // Perform common initialization for Fennec/GeckoView.
- GeckoAppShell.setLayerView(this);
-
- initializeView(EventDispatcher.getInstance());
- }
-
- @Override
- protected Parcelable onSaveInstanceState()
- {
- final Parcelable superState = super.onSaveInstanceState();
- stateSaved = true;
- return new StateBinder(superState, this.window);
- }
-
- @Override
- protected void onRestoreInstanceState(final Parcelable state)
- {
- final StateBinder stateBinder = (StateBinder) state;
- // We have to always call super.onRestoreInstanceState because View keeps
- // track of these calls and throws an exception when we don't call it.
- super.onRestoreInstanceState(stateBinder.superState);
-
- if (stateBinder.window != null) {
- this.window = stateBinder.window;
- }
- stateSaved = false;
- }
-
- @Override
- public void onAttachedToWindow()
- {
- final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
-
- if (window == null) {
- // Open a new nsWindow if we didn't have one from before.
- window = new Window();
- final String chromeURI = getGeckoInterface().getDefaultChromeURI();
-
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- Window.open(window, this, window.glController,
- chromeURI,
- metrics.widthPixels, metrics.heightPixels);
- } else {
- GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, Window.class,
- "open", window, GeckoView.class, this, window.glController,
- String.class, chromeURI,
- metrics.widthPixels, metrics.heightPixels);
- }
- } else {
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- window.reattach(this);
- } else {
- GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
- window, "reattach", GeckoView.class, this);
- }
- }
-
- setGLController(window.glController);
- super.onAttachedToWindow();
- }
-
- @Override
- public void onDetachedFromWindow()
- {
- super.onDetachedFromWindow();
- super.destroy();
-
- if (stateSaved) {
- // If we saved state earlier, we don't want to close the nsWindow.
- return;
- }
-
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- window.close();
- window.disposeNative();
- } else {
- GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
- window, "close");
- GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
- window, "disposeNative");
- }
- }
-
- /* package */ void setInputConnectionListener(final InputConnectionListener icl) {
- mInputConnectionListener = icl;
- }
-
- @Override
- public Handler getHandler() {
- if (mInputConnectionListener != null) {
- return mInputConnectionListener.getHandler(super.getHandler());
- }
- return super.getHandler();
- }
-
- @Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- if (mInputConnectionListener != null) {
- return mInputConnectionListener.onCreateInputConnection(outAttrs);
- }
- return null;
- }
-
- @Override
- public boolean onKeyPreIme(int keyCode, KeyEvent event) {
- if (super.onKeyPreIme(keyCode, event)) {
- return true;
- }
- return mInputConnectionListener != null &&
- mInputConnectionListener.onKeyPreIme(keyCode, event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (super.onKeyUp(keyCode, event)) {
- return true;
- }
- return mInputConnectionListener != null &&
- mInputConnectionListener.onKeyUp(keyCode, event);
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (super.onKeyDown(keyCode, event)) {
- return true;
- }
- return mInputConnectionListener != null &&
- mInputConnectionListener.onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean onKeyLongPress(int keyCode, KeyEvent event) {
- if (super.onKeyLongPress(keyCode, event)) {
- return true;
- }
- return mInputConnectionListener != null &&
- mInputConnectionListener.onKeyLongPress(keyCode, event);
- }
-
- @Override
- public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
- if (super.onKeyMultiple(keyCode, repeatCount, event)) {
- return true;
- }
- return mInputConnectionListener != null &&
- mInputConnectionListener.onKeyMultiple(keyCode, repeatCount, event);
- }
-
- /* package */ boolean isIMEEnabled() {
- return mInputConnectionListener != null &&
- mInputConnectionListener.isIMEEnabled();
- }
-
- public void importScript(final String url) {
- if (url.startsWith("resource://android/assets/")) {
- GeckoAppShell.notifyObservers("GeckoView:ImportScript", url);
- return;
- }
-
- throw new IllegalArgumentException("Must import script from 'resources://android/assets/' location.");
- }
-
- public void connectToGecko() {
- GeckoAppShell.notifyObservers("Viewport:Flush", null);
- }
-
- private void handleReady(final JSONObject message) {
- connectToGecko();
-
- if (mChromeDelegate != null) {
- mChromeDelegate.onReady(this);
- }
- }
-
- private void handleStateChange(final JSONObject message) throws JSONException {
- int state = message.getInt("state");
- if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
- if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
- if (mContentDelegate != null) {
- int id = message.getInt("tabID");
- mContentDelegate.onPageStart(this, new Browser(id), message.getString("uri"));
- }
- } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
- if (mContentDelegate != null) {
- int id = message.getInt("tabID");
- mContentDelegate.onPageStop(this, new Browser(id), message.getBoolean("success"));
- }
- }
- }
- }
-
- private void handleLoadError(final JSONObject message) throws JSONException {
- if (mContentDelegate != null) {
- int id = message.getInt("tabID");
- mContentDelegate.onPageStop(GeckoView.this, new Browser(id), false);
- }
- }
-
- private void handlePageShow(final JSONObject message) throws JSONException {
- if (mContentDelegate != null) {
- int id = message.getInt("tabID");
- mContentDelegate.onPageShow(GeckoView.this, new Browser(id));
- }
- }
-
- private void handleTitleChanged(final JSONObject message) throws JSONException {
- if (mContentDelegate != null) {
- int id = message.getInt("tabID");
- mContentDelegate.onReceivedTitle(GeckoView.this, new Browser(id), message.getString("title"));
- }
- }
-
- private void handleLinkFavicon(final JSONObject message) throws JSONException {
- if (mContentDelegate != null) {
- int id = message.getInt("tabID");
- mContentDelegate.onReceivedFavicon(GeckoView.this, new Browser(id), message.getString("href"), message.getInt("size"));
- }
- }
-
- private void handlePrompt(final JSONObject message) throws JSONException {
- if (mChromeDelegate != null) {
- String hint = message.optString("hint");
- if ("alert".equals(hint)) {
- String text = message.optString("text");
- mChromeDelegate.onAlert(GeckoView.this, null, text, new PromptResult(message));
- } else if ("confirm".equals(hint)) {
- String text = message.optString("text");
- mChromeDelegate.onConfirm(GeckoView.this, null, text, new PromptResult(message));
- } else if ("prompt".equals(hint)) {
- String text = message.optString("text");
- String defaultValue = message.optString("textbox0");
- mChromeDelegate.onPrompt(GeckoView.this, null, text, defaultValue, new PromptResult(message));
- } else if ("remotedebug".equals(hint)) {
- mChromeDelegate.onDebugRequest(GeckoView.this, new PromptResult(message));
- }
- }
- }
-
- private void handleScriptMessage(final Bundle data, final EventCallback callback) {
- if (mChromeDelegate != null) {
- MessageResult result = null;
- if (callback != null) {
- result = new MessageResult(callback);
- }
- mChromeDelegate.onScriptMessage(GeckoView.this, data, result);
- }
- }
-
- /**
- * Set the chrome callback handler.
- * This will replace the current handler.
- * @param chrome An implementation of GeckoViewChrome.
- */
- public void setChromeDelegate(ChromeDelegate chrome) {
- mChromeDelegate = chrome;
- }
-
- /**
- * Set the content callback handler.
- * This will replace the current handler.
- * @param content An implementation of ContentDelegate.
- */
- public void setContentDelegate(ContentDelegate content) {
- mContentDelegate = content;
- }
-
- public static void setGeckoInterface(final BaseGeckoInterface geckoInterface) {
- GeckoAppShell.setGeckoInterface(geckoInterface);
- }
-
- public static GeckoAppShell.GeckoInterface getGeckoInterface() {
- return GeckoAppShell.getGeckoInterface();
- }
-
- protected String getSharedPreferencesFile() {
- return DEFAULT_SHARED_PREFERENCES_FILE;
- }
-
- @Override
- public SharedPreferences getSharedPreferences() {
- return getContext().getSharedPreferences(getSharedPreferencesFile(), 0);
- }
-
- /**
- * Wrapper for a browser in the GeckoView container. Associated with a browser
- * element in the Gecko system.
- */
- public class Browser {
- private final int mId;
- private Browser(int Id) {
- mId = Id;
- }
-
- /**
- * Get the ID of the Browser. This is the same ID used by Gecko for it's underlying
- * browser element.
- * @return The integer ID of the Browser.
- */
- private int getId() {
- return mId;
- }
-
- /**
- * Load a URL resource into the Browser.
- * @param url The URL string.
- */
- public void loadUrl(String url) {
- JSONObject args = new JSONObject();
- try {
- args.put("url", url);
- args.put("parentId", -1);
- args.put("newTab", false);
- args.put("tabID", mId);
- } catch (Exception e) {
- Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
- }
- GeckoAppShell.notifyObservers("Tab:Load", args.toString());
- }
- }
-
- /* Provides a means for the client to indicate whether a JavaScript
- * dialog request should proceed. An instance of this class is passed to
- * various GeckoViewChrome callback actions.
- */
- public class PromptResult {
- private final int RESULT_OK = 0;
- private final int RESULT_CANCEL = 1;
-
- private final JSONObject mMessage;
-
- public PromptResult(JSONObject message) {
- mMessage = message;
- }
-
- private JSONObject makeResult(int resultCode) {
- JSONObject result = new JSONObject();
- try {
- result.put("button", resultCode);
- } catch (JSONException ex) { }
- return result;
- }
-
- /**
- * Handle a confirmation response from the user.
- */
- public void confirm() {
- JSONObject result = makeResult(RESULT_OK);
- EventDispatcher.sendResponse(mMessage, result);
- }
-
- /**
- * Handle a confirmation response from the user.
- * @param value String value to return to the browser context.
- */
- public void confirmWithValue(String value) {
- JSONObject result = makeResult(RESULT_OK);
- try {
- result.put("textbox0", value);
- } catch (JSONException ex) { }
- EventDispatcher.sendResponse(mMessage, result);
- }
-
- /**
- * Handle a cancellation response from the user.
- */
- public void cancel() {
- JSONObject result = makeResult(RESULT_CANCEL);
- EventDispatcher.sendResponse(mMessage, result);
- }
- }
-
- /* Provides a means for the client to respond to a script message with some data.
- * An instance of this class is passed to GeckoViewChrome.onScriptMessage.
- */
- public class MessageResult {
- private final EventCallback mCallback;
-
- public MessageResult(EventCallback callback) {
- if (callback == null) {
- throw new IllegalArgumentException("EventCallback should not be null.");
- }
- mCallback = callback;
- }
-
- private JSONObject bundleToJSON(Bundle data) {
- JSONObject result = new JSONObject();
- if (data == null) {
- return result;
- }
-
- final Set<String> keys = data.keySet();
- for (String key : keys) {
- try {
- result.put(key, data.get(key));
- } catch (JSONException e) {
- }
- }
- return result;
- }
-
- /**
- * Handle a successful response to a script message.
- * @param value Bundle value to return to the script context.
- */
- public void success(Bundle data) {
- mCallback.sendSuccess(bundleToJSON(data));
- }
-
- /**
- * Handle a failure response to a script message.
- */
- public void failure(Bundle data) {
- mCallback.sendError(bundleToJSON(data));
- }
- }
-
- public interface ChromeDelegate {
- /**
- * Tell the host application that Gecko is ready to handle requests.
- * @param view The GeckoView that initiated the callback.
- */
- public void onReady(GeckoView view);
-
- /**
- * Tell the host application to display an alert dialog.
- * @param view The GeckoView that initiated the callback.
- * @param browser The Browser that is loading the content.
- * @param message The string to display in the dialog.
- * @param result A PromptResult used to send back the result without blocking.
- * Defaults to cancel requests.
- */
- public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
-
- /**
- * Tell the host application to display a confirmation dialog.
- * @param view The GeckoView that initiated the callback.
- * @param browser The Browser that is loading the content.
- * @param message The string to display in the dialog.
- * @param result A PromptResult used to send back the result without blocking.
- * Defaults to cancel requests.
- */
- public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
-
- /**
- * Tell the host application to display an input prompt dialog.
- * @param view The GeckoView that initiated the callback.
- * @param browser The Browser that is loading the content.
- * @param message The string to display in the dialog.
- * @param defaultValue The string to use as default input.
- * @param result A PromptResult used to send back the result without blocking.
- * Defaults to cancel requests.
- */
- public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result);
-
- /**
- * Tell the host application to display a remote debugging request dialog.
- * @param view The GeckoView that initiated the callback.
- * @param result A PromptResult used to send back the result without blocking.
- * Defaults to cancel requests.
- */
- public void onDebugRequest(GeckoView view, GeckoView.PromptResult result);
-
- /**
- * Receive a message from an imported script.
- * @param view The GeckoView that initiated the callback.
- * @param data Bundle of data sent with the message. Never null.
- * @param result A MessageResult used to send back a response without blocking. Can be null.
- * Defaults to do nothing.
- */
- public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result);
- }
-
- public interface ContentDelegate {
- /**
- * A Browser has started loading content from the network.
- * @param view The GeckoView that initiated the callback.
- * @param browser The Browser that is loading the content.
- * @param url The resource being loaded.
- */
- public void onPageStart(GeckoView view, GeckoView.Browser browser, String url);
-
- /**
- * A Browser has finished loading content from the network.
- * @param view The GeckoView that initiated the callback.
- * @param browser The Browser that was loading the content.
- * @param success Whether the page loaded successfully or an error occurred.
- */
- public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success);
-
- /**
- * A Browser is displaying content. This page could have been loaded via
- * network or from the session history.
- * @param view The GeckoView that initiated the callback.
- * @param browser The Browser that is showing the content.
- */
- public void onPageShow(GeckoView view, GeckoView.Browser browser);
-
- /**
- * A page title was discovered in the content or updated after the content
- * loaded.
- * @param view The GeckoView that initiated the callback.
- * @param browser The Browser that is showing the content.
- * @param title The title sent from the content.
- */
- public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title);
-
- /**
- * A link element was discovered in the content or updated after the content
- * loaded that specifies a favicon.
- * @param view The GeckoView that initiated the callback.
- * @param browser The Browser that is showing the content.
- * @param url The href of the link element specifying the favicon.
- * @param size The maximum size specified for the favicon, or -1 for any size.
- */
- public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/InputConnectionListener.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/* 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.Handler;
-import android.view.KeyEvent;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-
-/**
- * Interface for interacting with GeckoInputConnection from GeckoView.
- */
-interface InputConnectionListener
-{
- Handler getHandler(Handler defHandler);
- InputConnection onCreateInputConnection(EditorInfo outAttrs);
- boolean onKeyPreIme(int keyCode, KeyEvent event);
- boolean onKeyDown(int keyCode, KeyEvent event);
- boolean onKeyLongPress(int keyCode, KeyEvent event);
- boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event);
- boolean onKeyUp(int keyCode, KeyEvent event);
- boolean isIMEEnabled();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/InputMethods.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.util.Collection;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.content.Context;
-import android.provider.Settings.Secure;
-import android.view.inputmethod.InputMethodInfo;
-import android.view.inputmethod.InputMethodManager;
-
-final public class InputMethods {
- public static final String METHOD_ANDROID_LATINIME = "com.android.inputmethod.latin/.LatinIME";
- public static final String METHOD_ATOK = "com.justsystems.atokmobile.service/.AtokInputMethodService";
- public static final String METHOD_GOOGLE_JAPANESE_INPUT = "com.google.android.inputmethod.japanese/.MozcService";
- public static final String METHOD_GOOGLE_LATINIME = "com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME";
- public static final String METHOD_HTC_TOUCH_INPUT = "com.htc.android.htcime/.HTCIMEService";
- public static final String METHOD_IWNN = "jp.co.omronsoft.iwnnime.ml/.standardcommon.IWnnLanguageSwitcher";
- public static final String METHOD_OPENWNN_PLUS = "com.owplus.ime.openwnnplus/.OpenWnnJAJP";
- public static final String METHOD_SAMSUNG = "com.sec.android.inputmethod/.SamsungKeypad";
- public static final String METHOD_SIMEJI = "com.adamrocker.android.input.simeji/.OpenWnnSimeji";
- public static final String METHOD_SWIFTKEY = "com.touchtype.swiftkey/com.touchtype.KeyboardService";
- public static final String METHOD_SWYPE = "com.swype.android.inputmethod/.SwypeInputMethod";
- public static final String METHOD_SWYPE_BETA = "com.nuance.swype.input/.IME";
- public static final String METHOD_TOUCHPAL_KEYBOARD = "com.cootek.smartinputv5/com.cootek.smartinput5.TouchPalIME";
-
- private InputMethods() {}
-
- public static String getCurrentInputMethod(Context context) {
- String inputMethod = Secure.getString(context.getContentResolver(), Secure.DEFAULT_INPUT_METHOD);
- return (inputMethod != null ? inputMethod : "");
- }
-
- public static InputMethodInfo getInputMethodInfo(Context context, String inputMethod) {
- InputMethodManager imm = getInputMethodManager(context);
- Collection<InputMethodInfo> infos = imm.getEnabledInputMethodList();
- for (InputMethodInfo info : infos) {
- if (info.getId().equals(inputMethod)) {
- return info;
- }
- }
- return null;
- }
-
- public static InputMethodManager getInputMethodManager(Context context) {
- return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
- }
-
- public static boolean needsSoftResetWorkaround(String inputMethod) {
- // Stock latin IME on Android 4.2 and above
- return Versions.feature17Plus &&
- (METHOD_ANDROID_LATINIME.equals(inputMethod) ||
- METHOD_GOOGLE_LATINIME.equals(inputMethod));
- }
-
- public static boolean shouldCommitCharAsKey(String inputMethod) {
- return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
- }
-
- public static boolean isGestureKeyboard(Context context) {
- // SwiftKey is a gesture keyboard, but it doesn't seem to need any special-casing
- // to do AwesomeBar auto-spacing.
- String inputMethod = getCurrentInputMethod(context);
- return (Versions.feature17Plus &&
- (METHOD_ANDROID_LATINIME.equals(inputMethod) ||
- METHOD_GOOGLE_LATINIME.equals(inputMethod))) ||
- METHOD_SWYPE.equals(inputMethod) ||
- METHOD_SWYPE_BETA.equals(inputMethod) ||
- METHOD_TOUCHPAL_KEYBOARD.equals(inputMethod);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/NSSBridge.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/* 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.mozglue.GeckoLoader;
-
-import android.content.Context;
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-public class NSSBridge {
- private static final String LOGTAG = "NSSBridge";
-
- private static native String nativeEncrypt(String aDb, String aValue);
- private static native String nativeDecrypt(String aDb, String aValue);
-
- @RobocopTarget
- static public String encrypt(Context context, String aValue)
- throws Exception {
- String resourcePath = context.getPackageResourcePath();
- GeckoLoader.loadNSSLibs(context, resourcePath);
-
- String path = GeckoProfile.get(context).getDir().toString();
- return nativeEncrypt(path, aValue);
- }
-
- @RobocopTarget
- static public String encrypt(Context context, String profilePath, String aValue)
- throws Exception {
- String resourcePath = context.getPackageResourcePath();
- GeckoLoader.loadNSSLibs(context, resourcePath);
-
- return nativeEncrypt(profilePath, aValue);
- }
-
- @RobocopTarget
- static public String decrypt(Context context, String aValue)
- throws Exception {
- String resourcePath = context.getPackageResourcePath();
- GeckoLoader.loadNSSLibs(context, resourcePath);
-
- String path = GeckoProfile.get(context).getDir().toString();
- return nativeDecrypt(path, aValue);
- }
-
- @RobocopTarget
- static public String decrypt(Context context, String profilePath, String aValue)
- throws Exception {
- String resourcePath = context.getPackageResourcePath();
- GeckoLoader.loadNSSLibs(context, resourcePath);
-
- return nativeDecrypt(profilePath, aValue);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/NotificationClient.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/* -*- 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.Notification;
-import android.app.PendingIntent;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.util.LinkedList;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Client for posting notifications through a NotificationHandler.
- */
-public abstract class NotificationClient {
- private static final String LOGTAG = "GeckoNotificationClient";
-
- private volatile NotificationHandler mHandler;
- private boolean mReady;
- private final LinkedList<Runnable> mTaskQueue = new LinkedList<Runnable>();
- private final ConcurrentHashMap<Integer, UpdateRunnable> mUpdatesMap =
- new ConcurrentHashMap<Integer, UpdateRunnable>();
-
- /**
- * Runnable that is reused between update notifications.
- *
- * Updates happen frequently, so reusing Runnables prevents frequent dynamic allocation.
- */
- private class UpdateRunnable implements Runnable {
- private long mProgress;
- private long mProgressMax;
- private String mAlertText;
- final private int mNotificationID;
-
- public UpdateRunnable(int notificationID) {
- mNotificationID = notificationID;
- }
-
- public synchronized boolean updateProgress(long progress, long progressMax, String alertText) {
- if (progress == mProgress
- && mProgressMax == progressMax
- && TextUtils.equals(mAlertText, alertText)) {
- return false;
- }
-
- mProgress = progress;
- mProgressMax = progressMax;
- mAlertText = alertText;
- return true;
- }
-
- @Override
- public void run() {
- long progress;
- long progressMax;
- String alertText;
-
- synchronized (this) {
- progress = mProgress;
- progressMax = mProgressMax;
- alertText = mAlertText;
- }
-
- mHandler.update(mNotificationID, progress, progressMax, alertText);
- }
- };
-
- /**
- * Adds a notification.
- *
- * @see NotificationHandler#add(int, String, String, String, PendingIntent, PendingIntent)
- */
- public synchronized void add(final int notificationID, final String aImageUrl, final String aHost,
- final String aAlertTitle, final String aAlertText, final PendingIntent contentIntent) {
- mTaskQueue.add(new Runnable() {
- @Override
- public void run() {
- mHandler.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
- }
- });
- notify();
-
- if (!mReady) {
- bind();
- }
- }
-
- /**
- * Adds a notification.
- *
- * @see NotificationHandler#add(int, Notification)
- */
- public synchronized void add(final int notificationID, final Notification notification) {
- mTaskQueue.add(new Runnable() {
- @Override
- public void run() {
- mHandler.add(notificationID, notification);
- }
- });
- notify();
-
- if (!mReady) {
- bind();
- }
- }
-
- /**
- * Updates a notification.
- *
- * @see NotificationHandler#update(int, long, long, String)
- */
- public void update(final int notificationID, final long aProgress, final long aProgressMax,
- final String aAlertText) {
- UpdateRunnable runnable = mUpdatesMap.get(notificationID);
-
- if (runnable == null) {
- runnable = new UpdateRunnable(notificationID);
- mUpdatesMap.put(notificationID, runnable);
- }
-
- // If we've already posted an update with these values, there's no
- // need to do it again.
- if (!runnable.updateProgress(aProgress, aProgressMax, aAlertText)) {
- return;
- }
-
- synchronized (this) {
- if (mReady) {
- mTaskQueue.add(runnable);
- notify();
- }
- }
- }
-
- /**
- * Removes a notification.
- *
- * @see NotificationHandler#remove(int)
- */
- public synchronized void remove(final int notificationID) {
- mTaskQueue.add(new Runnable() {
- @Override
- public void run() {
- mHandler.remove(notificationID);
- mUpdatesMap.remove(notificationID);
- }
- });
-
- // If mReady == false, we haven't added any notifications yet. That can happen if Fennec is being
- // started in response to clicking a notification. Call bind() to ensure the task we posted above is run.
- if (!mReady) {
- bind();
- }
-
- notify();
- }
-
- /**
- * Determines whether a notification is showing progress.
- *
- * @see NotificationHandler#isProgressStyle(int)
- */
- public boolean isOngoing(int notificationID) {
- final NotificationHandler handler = mHandler;
- return handler != null && handler.isOngoing(notificationID);
- }
-
- protected void bind() {
- mReady = true;
- }
-
- protected void unbind() {
- mReady = false;
- mUpdatesMap.clear();
- }
-
- protected void connectHandler(NotificationHandler handler) {
- mHandler = handler;
- new Thread(new NotificationRunnable()).start();
- }
-
- private class NotificationRunnable implements Runnable {
- @Override
- public void run() {
- Runnable r;
- try {
- while (true) {
- // Synchronize polls to prevent tasks from being added to the queue
- // during the isDone check.
- synchronized (NotificationClient.this) {
- r = mTaskQueue.poll();
- while (r == null) {
- if (mHandler.isDone()) {
- unbind();
- return;
- }
- NotificationClient.this.wait();
- r = mTaskQueue.poll();
- }
- }
- r.run();
- }
- } catch (InterruptedException e) {
- Log.e(LOGTAG, "Notification task queue processing interrupted", e);
- }
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/NotificationHandler.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/* -*- 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.graphics.Bitmap;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationManagerCompat;
-import org.mozilla.gecko.gfx.BitmapUtils;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-public class NotificationHandler {
- private static String LOGTAG = "GeckoNotifHandler";
- private final ConcurrentHashMap<Integer, Notification>
- mNotifications = new ConcurrentHashMap<Integer, Notification>();
- private final Context mContext;
- private final NotificationManagerCompat mNotificationManager;
-
- /**
- * Notification associated with this service's foreground state.
- *
- * {@link android.app.Service#startForeground(int, android.app.Notification)}
- * associates the foreground with exactly one notification from the service.
- * To keep Fennec alive during downloads (and to make sure it can be killed
- * once downloads are complete), we make sure that the foreground is always
- * associated with an active progress notification if and only if at least
- * one download is in progress.
- */
- private Notification mForegroundNotification;
- private int mForegroundNotificationId;
-
- public NotificationHandler(Context context) {
- mContext = context;
- mNotificationManager = NotificationManagerCompat.from(context);
- }
-
- /**
- * Adds a notification.
- *
- * @param notificationID the unique ID of the notification
- * @param aImageUrl URL of the image to use
- * @param aAlertTitle title of the notification
- * @param aAlertText text of the notification
- * @param contentIntent Intent used when the notification is clicked
- */
- public void add(final int notificationID, String aImageUrl, String aHost, String aAlertTitle,
- String aAlertText, PendingIntent contentIntent) {
- // Remove the old notification with the same ID, if any
- remove(notificationID);
-
- final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
- .setContentTitle(aAlertTitle)
- .setContentText(aAlertText)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setContentIntent(contentIntent)
- .setAutoCancel(true)
- .setStyle(new NotificationCompat.InboxStyle()
- .addLine(aAlertText)
- .setSummaryText(aHost));
-
- // Fetch icon.
- if (!aImageUrl.isEmpty()) {
- final Bitmap image = BitmapUtils.decodeUrl(aImageUrl);
- builder.setLargeIcon(image);
- }
-
- builder.setWhen(System.currentTimeMillis());
- final Notification notification = builder.build();
-
- mNotificationManager.notify(notificationID, notification);
- mNotifications.put(notificationID, notification);
- }
-
- /**
- * Adds a notification.
- *
- * @param id the unique ID of the notification
- * @param notification the Notification to add
- */
- public void add(int id, Notification notification) {
- mNotificationManager.notify(id, notification);
- mNotifications.put(id, notification);
-
- if (mForegroundNotification == null && isOngoing(notification)) {
- setForegroundNotification(id, notification);
- }
- }
-
- /**
- * Updates a notification.
- *
- * @param notificationID ID of existing notification
- * @param aProgress progress of item being updated
- * @param aProgressMax max progress of item being updated
- * @param aAlertText text of the notification
- */
- public void update(int notificationID, long aProgress, long aProgressMax, String aAlertText) {
- Notification notification = mNotifications.get(notificationID);
- if (notification == null) {
- return;
- }
-
- notification = new NotificationCompat.Builder(mContext)
- .setContentText(aAlertText)
- .setSmallIcon(notification.icon)
- .setWhen(notification.when)
- .setContentIntent(notification.contentIntent)
- .setProgress((int) aProgressMax, (int) aProgress, false)
- .build();
-
- add(notificationID, notification);
- }
-
- /**
- * Removes a notification.
- *
- * @param notificationID ID of existing notification
- */
- public void remove(int notificationID) {
- final Notification notification = mNotifications.remove(notificationID);
- if (notification != null) {
- updateForegroundNotification(notificationID, notification);
- }
- mNotificationManager.cancel(notificationID);
- }
-
- /**
- * Determines whether the service is done.
- *
- * The service is considered finished when all notifications have been
- * removed.
- *
- * @return whether all notifications have been removed
- */
- public boolean isDone() {
- return mNotifications.isEmpty();
- }
-
- /**
- * Determines whether a notification should hold a foreground service to keep Gecko alive
- *
- * @param notificationID the id of the notification to check
- * @return whether the notification is ongoing
- */
- public boolean isOngoing(int notificationID) {
- final Notification notification = mNotifications.get(notificationID);
- return isOngoing(notification);
- }
-
- /**
- * Determines whether a notification should hold a foreground service to keep Gecko alive
- *
- * @param notification the notification to check
- * @return whether the notification is ongoing
- */
- public boolean isOngoing(Notification notification) {
- if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) > 0) {
- return true;
- }
- return false;
- }
-
- protected void setForegroundNotification(int id, Notification notification) {
- mForegroundNotificationId = id;
- mForegroundNotification = notification;
- }
-
- private void updateForegroundNotification(int oldId, Notification oldNotification) {
- if (mForegroundNotificationId == oldId) {
- // If we're removing the notification associated with the
- // foreground, we need to pick another active notification to act
- // as the foreground notification.
- Notification foregroundNotification = null;
- int foregroundId = 0;
- for (final Integer id : mNotifications.keySet()) {
- final Notification notification = mNotifications.get(id);
- if (isOngoing(notification)) {
- foregroundNotification = notification;
- foregroundId = id;
- break;
- }
- }
- setForegroundNotification(foregroundId, foregroundNotification);
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/NotificationService.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- 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.Notification;
-import android.app.Service;
-import android.content.Intent;
-import android.os.Binder;
-import android.os.IBinder;
-
-public class NotificationService extends Service {
- private final IBinder mBinder = new NotificationBinder();
- private NotificationHandler mHandler;
-
- @Override
- public void onCreate() {
- // This has to be initialized in onCreate in order to ensure that the NotificationHandler can
- // access the NotificationManager service.
- mHandler = new NotificationHandler(this) {
- @Override
- protected void setForegroundNotification(int id, Notification notification) {
- super.setForegroundNotification(id, notification);
-
- if (notification == null) {
- stopForeground(true);
- } else {
- startForeground(id, notification);
- }
- }
- };
- }
-
- public class NotificationBinder extends Binder {
- NotificationService getService() {
- // Return this instance of NotificationService so clients can call public methods
- return NotificationService.this;
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
-
- public NotificationHandler getNotificationHandler() {
- return mHandler;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/PrefsHelper.java
+++ /dev/null
@@ -1,308 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-import android.support.v4.util.SimpleArrayMap;
-import android.util.Log;
-import android.util.SparseArray;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * Helper class to get/set gecko prefs.
- */
-public final class PrefsHelper {
- private static final String LOGTAG = "GeckoPrefsHelper";
-
- // Map pref name to ArrayList for multiple observers or PrefHandler for single observer.
- private static final SimpleArrayMap<String, Object> OBSERVERS = new SimpleArrayMap<>();
- private static final HashSet<String> INT_TO_STRING_PREFS = new HashSet<>(8);
- private static final HashSet<String> INT_TO_BOOL_PREFS = new HashSet<>(2);
-
- static {
- INT_TO_STRING_PREFS.add("browser.chrome.titlebarMode");
- INT_TO_STRING_PREFS.add("network.cookie.cookieBehavior");
- INT_TO_STRING_PREFS.add("font.size.inflation.minTwips");
- INT_TO_STRING_PREFS.add("home.sync.updateMode");
- INT_TO_STRING_PREFS.add("browser.image_blocking");
- INT_TO_BOOL_PREFS.add("browser.display.use_document_fonts");
- }
-
- @WrapForJNI
- private static final int PREF_INVALID = -1;
- @WrapForJNI
- private static final int PREF_FINISH = 0;
- @WrapForJNI
- private static final int PREF_BOOL = 1;
- @WrapForJNI
- private static final int PREF_INT = 2;
- @WrapForJNI
- private static final int PREF_STRING = 3;
-
- @WrapForJNI(stubName = "GetPrefs")
- private static native void nativeGetPrefs(String[] prefNames, PrefHandler handler);
- @WrapForJNI(stubName = "SetPref")
- private static native void nativeSetPref(String prefName, boolean flush, int type,
- boolean boolVal, int intVal, String strVal);
- @WrapForJNI(stubName = "AddObserver")
- private static native void nativeAddObserver(String[] prefNames, PrefHandler handler,
- String[] prefsToObserve);
- @WrapForJNI(stubName = "RemoveObserver")
- private static native void nativeRemoveObserver(String[] prefToUnobserve);
-
- @RobocopTarget
- public static void getPrefs(final String[] prefNames, final PrefHandler callback) {
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- nativeGetPrefs(prefNames, callback);
- } else {
- GeckoThread.queueNativeCallUntil(
- GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeGetPrefs",
- String[].class, prefNames, PrefHandler.class, callback);
- }
- }
-
- public static void getPref(final String prefName, final PrefHandler callback) {
- getPrefs(new String[] { prefName }, callback);
- }
-
- public static void getPrefs(final ArrayList<String> prefNames, final PrefHandler callback) {
- getPrefs(prefNames.toArray(new String[prefNames.size()]), callback);
- }
-
- @RobocopTarget
- public static void setPref(final String pref, final Object value, final boolean flush) {
- final int type;
- boolean boolVal = false;
- int intVal = 0;
- String strVal = null;
-
- if (INT_TO_STRING_PREFS.contains(pref)) {
- // When sending to Java, we normalized special preferences that use integers
- // and strings to represent booleans. Here, we convert them back to their
- // actual types so we can store them.
- type = PREF_INT;
- intVal = Integer.parseInt(String.valueOf(value));
- } else if (INT_TO_BOOL_PREFS.contains(pref)) {
- type = PREF_INT;
- intVal = (Boolean) value ? 1 : 0;
- } else if (value instanceof Boolean) {
- type = PREF_BOOL;
- boolVal = (Boolean) value;
- } else if (value instanceof Integer) {
- type = PREF_INT;
- intVal = (Integer) value;
- } else {
- type = PREF_STRING;
- strVal = String.valueOf(value);
- }
-
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- nativeSetPref(pref, flush, type, boolVal, intVal, strVal);
- } else {
- GeckoThread.queueNativeCallUntil(
- GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeSetPref",
- String.class, pref, flush, type, boolVal, intVal, String.class, strVal);
- }
- }
-
- public static void setPref(final String pref, final Object value) {
- setPref(pref, value, /* flush */ false);
- }
-
- @RobocopTarget
- public synchronized static void addObserver(final String[] prefNames,
- final PrefHandler handler) {
- List<String> prefsToObserve = null;
-
- for (String pref : prefNames) {
- final Object existing = OBSERVERS.get(pref);
-
- if (existing == null) {
- // Not observing yet, so add observer.
- if (prefsToObserve == null) {
- prefsToObserve = new ArrayList<>(prefNames.length);
- }
- prefsToObserve.add(pref);
- OBSERVERS.put(pref, handler);
-
- } else if (existing instanceof PrefHandler) {
- // Already observing one, so turn it into an array.
- final List<PrefHandler> handlerList = new ArrayList<>(2);
- handlerList.add((PrefHandler) existing);
- handlerList.add(handler);
- OBSERVERS.put(pref, handlerList);
-
- } else {
- // Already observing multiple, so add to existing array.
- @SuppressWarnings("unchecked")
- final List<PrefHandler> handlerList = (List) existing;
- handlerList.add(handler);
- }
- }
-
- final String[] namesToObserve = prefsToObserve == null ? null :
- prefsToObserve.toArray(new String[prefsToObserve.size()]);
-
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- nativeAddObserver(prefNames, handler, namesToObserve);
- } else {
- GeckoThread.queueNativeCallUntil(
- GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeAddObserver",
- String[].class, prefNames, PrefHandler.class, handler,
- String[].class, namesToObserve);
- }
- }
-
- @RobocopTarget
- public synchronized static void removeObserver(final PrefHandler handler) {
- List<String> prefsToUnobserve = null;
-
- for (int i = OBSERVERS.size() - 1; i >= 0; i--) {
- final Object existing = OBSERVERS.valueAt(i);
- boolean removeObserver = false;
-
- if (existing == handler) {
- removeObserver = true;
-
- } else if (!(existing instanceof PrefHandler)) {
- // Removing existing handler from list.
- @SuppressWarnings("unchecked")
- final List<PrefHandler> handlerList = (List) existing;
- if (handlerList.remove(handler) && handlerList.isEmpty()) {
- removeObserver = true;
- }
- }
-
- if (removeObserver) {
- // Removed last handler, so remove observer.
- if (prefsToUnobserve == null) {
- prefsToUnobserve = new ArrayList<>();
- }
- prefsToUnobserve.add(OBSERVERS.keyAt(i));
- OBSERVERS.removeAt(i);
- }
- }
-
- if (prefsToUnobserve == null) {
- return;
- }
-
- final String[] namesToUnobserve =
- prefsToUnobserve.toArray(new String[prefsToUnobserve.size()]);
-
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- nativeRemoveObserver(namesToUnobserve);
- } else {
- GeckoThread.queueNativeCallUntil(
- GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeRemoveObserver",
- String[].class, namesToUnobserve);
- }
- }
-
- @WrapForJNI
- private static void callPrefHandler(final PrefHandler handler, int type, final String pref,
- boolean boolVal, int intVal, String strVal) {
-
- // Some Gecko preferences use integers or strings to reference state instead of
- // directly representing the value. Since the Java UI uses the type to determine
- // which ui elements to show and how to handle them, we need to normalize these
- // preferences to the correct type.
- if (INT_TO_STRING_PREFS.contains(pref)) {
- type = PREF_STRING;
- strVal = String.valueOf(intVal);
- } else if (INT_TO_BOOL_PREFS.contains(pref)) {
- type = PREF_BOOL;
- boolVal = intVal == 1;
- }
-
- switch (type) {
- case PREF_FINISH:
- handler.finish();
- return;
- case PREF_BOOL:
- handler.prefValue(pref, boolVal);
- return;
- case PREF_INT:
- handler.prefValue(pref, intVal);
- return;
- case PREF_STRING:
- handler.prefValue(pref, strVal);
- return;
- }
- throw new IllegalArgumentException();
- }
-
- @WrapForJNI
- private synchronized static void onPrefChange(final String pref, final int type,
- final boolean boolVal, final int intVal,
- final String strVal) {
- final Object existing = OBSERVERS.get(pref);
-
- if (existing == null) {
- return;
- }
-
- final Iterator<PrefHandler> itor;
- PrefHandler handler;
-
- if (existing instanceof PrefHandler) {
- itor = null;
- handler = (PrefHandler) existing;
- } else {
- @SuppressWarnings("unchecked")
- final List<PrefHandler> handlerList = (List) existing;
- if (handlerList.isEmpty()) {
- return;
- }
- itor = handlerList.iterator();
- handler = itor.next();
- }
-
- do {
- callPrefHandler(handler, type, pref, boolVal, intVal, strVal);
- handler.finish();
-
- handler = itor != null && itor.hasNext() ? itor.next() : null;
- } while (handler != null);
- }
-
- public interface PrefHandler {
- void prefValue(String pref, boolean value);
- void prefValue(String pref, int value);
- void prefValue(String pref, String value);
- void finish();
- }
-
- public static abstract class PrefHandlerBase implements PrefHandler {
- @Override
- public void prefValue(String pref, boolean value) {
- throw new UnsupportedOperationException(
- "Unhandled boolean pref " + pref + "; wrong type?");
- }
-
- @Override
- public void prefValue(String pref, int value) {
- throw new UnsupportedOperationException(
- "Unhandled int pref " + pref + "; wrong type?");
- }
-
- @Override
- public void prefValue(String pref, String value) {
- throw new UnsupportedOperationException(
- "Unhandled String pref " + pref + "; wrong type?");
- }
-
- @Override
- public void finish() {
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/ServiceNotificationClient.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/* -*- 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.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.IBinder;
-import android.util.Log;
-
-/**
- * Client for posting notifications through the NotificationService.
- */
-public class ServiceNotificationClient extends NotificationClient {
- private static final String LOGTAG = "GeckoServiceNotificationClient";
-
- private final ServiceConnection mConnection = new NotificationServiceConnection();
- private boolean mBound;
- private final Context mContext;
-
- public ServiceNotificationClient(Context context) {
- mContext = context;
- }
-
- @Override
- protected void bind() {
- super.bind();
- final Intent intent = new Intent(mContext, NotificationService.class);
- mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
- }
-
- @Override
- protected void unbind() {
- // If there are no more tasks and no notifications being
- // displayed, the service is disconnected. Unfortunately,
- // since completed download notifications are shown by
- // removing the progress notification and creating a new
- // static one, this will cause the service to be unbound
- // and immediately rebound when a download completes.
- super.unbind();
-
- if (mBound) {
- mBound = false;
- mContext.unbindService(mConnection);
- }
- }
-
- class NotificationServiceConnection implements ServiceConnection {
- @Override
- public void onServiceConnected(ComponentName className, IBinder service) {
- final NotificationService.NotificationBinder binder =
- (NotificationService.NotificationBinder) service;
- connectHandler(binder.getService().getNotificationHandler());
- mBound = true;
- }
-
- @Override
- public void onServiceDisconnected(ComponentName componentName) {
- // This is called when the connection with the service has been
- // unexpectedly disconnected -- that is, its process crashed.
- // Because it is running in our same process, we should never
- // see this happen, and the correctness of this class relies on
- // this never happening.
- Log.e(LOGTAG, "Notification service disconnected", new Exception());
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/SmsManager.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/* -*- 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;
-
-public class SmsManager {
- private static final ISmsManager sInstance;
- static {
- if (AppConstants.MOZ_WEBSMS_BACKEND) {
- sInstance = new GeckoSmsManager();
- } else {
- sInstance = null;
- }
- }
-
- public static ISmsManager getInstance() {
- return sInstance;
- }
-
- public static boolean isEnabled() {
- return AppConstants.MOZ_WEBSMS_BACKEND;
- }
-
- public interface ISmsManager {
- void start();
- void stop();
- void shutdown();
-
- void send(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify);
- void getMessage(int aMessageId, int aRequestId);
- void deleteMessage(int aMessageId, int aRequestId);
- void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId);
- void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId);
- void createThreadCursor(int aRequestId);
- void getNextThread(int aRequestId);
- void getNextMessage(int aRequestId);
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/SurfaceBits.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/* 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 java.nio.ByteBuffer;
-
-@WrapForJNI
-public class SurfaceBits {
- public int width;
- public int height;
- public int format;
- public ByteBuffer buffer;
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/TouchEventInterceptor.java
+++ /dev/null
@@ -1,14 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.view.MotionEvent;
-import android.view.View;
-
-public interface TouchEventInterceptor extends View.OnTouchListener {
- /** Override this method for a chance to consume events before the view or its children */
- public boolean onInterceptTouchEvent(View view, MotionEvent event);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/BitmapUtils.java
+++ /dev/null
@@ -1,466 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Field;
-import java.net.MalformedURLException;
-import java.net.URL;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.GeckoJarReader;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Log;
-
-public final class BitmapUtils {
- /* Default colors. */
- private static final float[] DEFAULT_LAUNCHER_ICON_HSV = { 32.0f, 1.0f, 1.0f };
-
- private static final String LOGTAG = "GeckoBitmapUtils";
-
- private BitmapUtils() {}
-
- public interface BitmapLoader {
- public void onBitmapFound(Drawable d);
- }
-
- private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) {
- if (ThreadUtils.isOnUiThread()) {
- loader.onBitmapFound(d);
- return;
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- loader.onBitmapFound(d);
- }
- });
- }
-
- /**
- * Attempts to find a drawable associated with a given string, using its URI scheme to determine
- * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and
- * will be called with `null` if no drawable is found.
- *
- * The BitmapLoader `onBitmapFound` method always runs on the UI thread.
- */
- public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
- if (TextUtils.isEmpty(data)) {
- runOnBitmapFoundOnUiThread(loader, null);
- return;
- }
-
- if (data.startsWith("data")) {
- final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
- runOnBitmapFoundOnUiThread(loader, d);
- return;
- }
-
- if (data.startsWith("jar:") || data.startsWith("file://")) {
- (new UIAsyncTask.WithoutParams<Drawable>(ThreadUtils.getBackgroundHandler()) {
- @Override
- public Drawable doInBackground() {
- try {
- if (data.startsWith("jar:jar")) {
- return GeckoJarReader.getBitmapDrawable(
- context, context.getResources(), data);
- }
-
- // Don't attempt to validate the JAR signature when loading an add-on icon
- if (data.startsWith("jar:file")) {
- return GeckoJarReader.getBitmapDrawable(
- context, context.getResources(), Uri.decode(data));
- }
-
- final URL url = new URL(data);
- final InputStream is = (InputStream) url.getContent();
- try {
- return Drawable.createFromStream(is, "src");
- } finally {
- is.close();
- }
- } catch (Exception e) {
- Log.w(LOGTAG, "Unable to set icon", e);
- }
- return null;
- }
-
- @Override
- public void onPostExecute(Drawable drawable) {
- loader.onBitmapFound(drawable);
- }
- }).execute();
- return;
- }
-
- if (data.startsWith("-moz-icon://")) {
- final Uri imageUri = Uri.parse(data);
- final String ssp = imageUri.getSchemeSpecificPart();
- final String resource = ssp.substring(ssp.lastIndexOf('/') + 1);
-
- try {
- final Drawable d = context.getPackageManager().getApplicationIcon(resource);
- runOnBitmapFoundOnUiThread(loader, d);
- } catch (Exception ex) { }
-
- return;
- }
-
- if (data.startsWith("drawable://")) {
- final Uri imageUri = Uri.parse(data);
- final int id = getResource(imageUri, R.drawable.ic_status_logo);
- final Drawable d = context.getResources().getDrawable(id);
-
- runOnBitmapFoundOnUiThread(loader, d);
- return;
- }
-
- runOnBitmapFoundOnUiThread(loader, null);
- }
-
- public static Bitmap decodeByteArray(byte[] bytes) {
- return decodeByteArray(bytes, null);
- }
-
- public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
- return decodeByteArray(bytes, 0, bytes.length, options);
- }
-
- public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
- return decodeByteArray(bytes, offset, length, null);
- }
-
- public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
- if (bytes.length <= 0) {
- throw new IllegalArgumentException("bytes.length " + bytes.length
- + " must be a positive number");
- }
-
- Bitmap bitmap = null;
- try {
- bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
- } catch (OutOfMemoryError e) {
- Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
- + ", options= " + options + ") OOM!", e);
- return null;
- }
-
- if (bitmap == null) {
- Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
- return null;
- }
-
- if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
- Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned "
- + "a bitmap with dimensions " + bitmap.getWidth()
- + "x" + bitmap.getHeight());
- return null;
- }
-
- return bitmap;
- }
-
- public static Bitmap decodeStream(InputStream inputStream) {
- try {
- return BitmapFactory.decodeStream(inputStream);
- } catch (OutOfMemoryError e) {
- Log.e(LOGTAG, "decodeStream() OOM!", e);
- return null;
- }
- }
-
- public static Bitmap decodeUrl(Uri uri) {
- return decodeUrl(uri.toString());
- }
-
- public static Bitmap decodeUrl(String urlString) {
- URL url;
-
- try {
- url = new URL(urlString);
- } catch (MalformedURLException e) {
- Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
- return null;
- }
-
- return decodeUrl(url);
- }
-
- public static Bitmap decodeUrl(URL url) {
- InputStream stream = null;
-
- try {
- stream = url.openStream();
- } catch (IOException e) {
- Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
- return null;
- }
-
- if (stream == null) {
- Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
- return null;
- }
-
- Bitmap bitmap = decodeStream(stream);
-
- try {
- stream.close();
- } catch (IOException e) {
- Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
- }
-
- return bitmap;
- }
-
- public static Bitmap decodeResource(Context context, int id) {
- return decodeResource(context, id, null);
- }
-
- public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) {
- Resources resources = context.getResources();
- try {
- return BitmapFactory.decodeResource(resources, id, options);
- } catch (OutOfMemoryError e) {
- Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e);
- return null;
- }
- }
-
- public static int getDominantColor(Bitmap source) {
- return getDominantColor(source, true);
- }
-
- public static int getDominantColor(Bitmap source, boolean applyThreshold) {
- if (source == null)
- return Color.argb(255, 255, 255, 255);
-
- // Keep track of how many times a hue in a given bin appears in the image.
- // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
- int[] colorBins = new int[36];
-
- // The bin with the most colors. Initialize to -1 to prevent accidentally
- // thinking the first bin holds the dominant color.
- int maxBin = -1;
-
- // Keep track of sum hue/saturation/value per hue bin, which we'll use to
- // compute an average to for the dominant color.
- float[] sumHue = new float[36];
- float[] sumSat = new float[36];
- float[] sumVal = new float[36];
- float[] hsv = new float[3];
-
- int height = source.getHeight();
- int width = source.getWidth();
- int[] pixels = new int[width * height];
- source.getPixels(pixels, 0, width, 0, 0, width, height);
- for (int row = 0; row < height; row++) {
- for (int col = 0; col < width; col++) {
- int c = pixels[col + row * width];
- // Ignore pixels with a certain transparency.
- if (Color.alpha(c) < 128)
- continue;
-
- Color.colorToHSV(c, hsv);
-
- // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
- if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
- continue;
-
- // We compute the dominant color by putting colors in bins based on their hue.
- int bin = (int) Math.floor(hsv[0] / 10.0f);
-
- // Update the sum hue/saturation/value for this bin.
- sumHue[bin] = sumHue[bin] + hsv[0];
- sumSat[bin] = sumSat[bin] + hsv[1];
- sumVal[bin] = sumVal[bin] + hsv[2];
-
- // Increment the number of colors in this bin.
- colorBins[bin]++;
-
- // Keep track of the bin that holds the most colors.
- if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
- maxBin = bin;
- }
- }
-
- // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
- if (maxBin < 0)
- return Color.argb(255, 255, 255, 255);
-
- // Return a color with the average hue/saturation/value of the bin with the most colors.
- hsv[0] = sumHue[maxBin] / colorBins[maxBin];
- hsv[1] = sumSat[maxBin] / colorBins[maxBin];
- hsv[2] = sumVal[maxBin] / colorBins[maxBin];
- return Color.HSVToColor(hsv);
- }
-
- /**
- * Decodes a bitmap from a Base64 data URI.
- *
- * @param dataURI a Base64-encoded data URI string
- * @return the decoded bitmap, or null if the data URI is invalid
- */
- public static Bitmap getBitmapFromDataURI(String dataURI) {
- if (dataURI == null) {
- return null;
- }
-
- byte[] raw = getBytesFromDataURI(dataURI);
- if (raw == null || raw.length == 0) {
- return null;
- }
-
- return decodeByteArray(raw);
- }
-
- /**
- * Return a byte[] containing the bytes in a given base64 string, or null if this is not a valid
- * base64 string.
- */
- public static byte[] getBytesFromBase64(String base64) {
- try {
- return Base64.decode(base64, Base64.DEFAULT);
- } catch (Exception e) {
- Log.e(LOGTAG, "exception decoding bitmap from data URI: " + base64, e);
- }
-
- return null;
- }
-
- public static byte[] getBytesFromDataURI(String dataURI) {
- final String base64 = dataURI.substring(dataURI.indexOf(',') + 1);
- return getBytesFromBase64(base64);
- }
-
- public static Bitmap getBitmapFromDrawable(Drawable drawable) {
- if (drawable instanceof BitmapDrawable) {
- return ((BitmapDrawable) drawable).getBitmap();
- }
-
- int width = drawable.getIntrinsicWidth();
- width = width > 0 ? width : 1;
- int height = drawable.getIntrinsicHeight();
- height = height > 0 ? height : 1;
-
- Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
- drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- drawable.draw(canvas);
-
- return bitmap;
- }
-
- public static int getResource(Uri resourceUrl, int defaultIcon) {
- int icon = defaultIcon;
-
- final String scheme = resourceUrl.getScheme();
- if ("drawable".equals(scheme)) {
- String resource = resourceUrl.getSchemeSpecificPart();
- resource = resource.substring(resource.lastIndexOf('/') + 1);
-
- try {
- return Integer.parseInt(resource);
- } catch (NumberFormatException ex) {
- // This isn't a resource id, try looking for a string
- }
-
- try {
- final Class<R.drawable> drawableClass = R.drawable.class;
- final Field f = drawableClass.getField(resource);
- icon = f.getInt(null);
- } catch (final NoSuchFieldException e1) {
-
- // just means the resource doesn't exist for fennec. Check in Android resources
- try {
- final Class<android.R.drawable> drawableClass = android.R.drawable.class;
- final Field f = drawableClass.getField(resource);
- icon = f.getInt(null);
- } catch (final NoSuchFieldException e2) {
- // This drawable doesn't seem to exist...
- } catch (Exception e3) {
- Log.i(LOGTAG, "Exception getting drawable", e3);
- }
-
- } catch (Exception e4) {
- Log.i(LOGTAG, "Exception getting drawable", e4);
- }
-
- resourceUrl = null;
- }
- return icon;
- }
-
- public static Bitmap getLauncherIcon(Context context, Bitmap aSource, int size) {
- final int kOffset = 6;
- final int kRadius = 5;
- int insetSize = aSource != null ? size * 2 / 3 : size;
-
- Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
-
-
- // draw a base color
- Paint paint = new Paint();
- if (aSource == null) {
- // If we aren't drawing a favicon, just use an orange color.
- paint.setColor(Color.HSVToColor(DEFAULT_LAUNCHER_ICON_HSV));
- canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
- } else if (aSource.getWidth() >= insetSize || aSource.getHeight() >= insetSize) {
- // Otherwise, if the icon is large enough, just draw it.
- Rect iconBounds = new Rect(0, 0, size, size);
- canvas.drawBitmap(aSource, null, iconBounds, null);
- return bitmap;
- } else {
- // otherwise use the dominant color from the icon + a layer of transparent white to lighten it somewhat
- int color = BitmapUtils.getDominantColor(aSource);
- paint.setColor(color);
- canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
- paint.setColor(Color.argb(100, 255, 255, 255));
- canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
- }
-
- // draw the overlay
- Bitmap overlay = BitmapUtils.decodeResource(context, R.drawable.home_bg);
- canvas.drawBitmap(overlay, null, new Rect(0, 0, size, size), null);
-
- // draw the favicon
- if (aSource == null)
- aSource = BitmapUtils.decodeResource(context, R.drawable.home_star);
-
- // by default, we scale the icon to this size
- int sWidth = insetSize / 2;
- int sHeight = sWidth;
-
- int halfSize = size / 2;
- canvas.drawBitmap(aSource,
- null,
- new Rect(halfSize - sWidth,
- halfSize - sHeight,
- halfSize + sWidth,
- halfSize + sHeight),
- null);
-
- return bitmap;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
+++ /dev/null
@@ -1,771 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.util.FloatUtils;
-
-import org.json.JSONArray;
-
-import android.graphics.PointF;
-import android.graphics.RectF;
-import android.util.Log;
-
-import java.util.HashMap;
-import java.util.Map;
-
-final class DisplayPortCalculator {
- private static final String LOGTAG = "GeckoDisplayPort";
- private static final PointF ZERO_VELOCITY = new PointF(0, 0);
-
- private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy";
- private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier";
- private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x";
- private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y";
- private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier";
- private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold";
- private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer";
- private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base";
- private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base";
- private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr";
- private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr";
- private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold";
-
- private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null);
-
- static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
- return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
- }
-
- static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
- if (displayPort == null) {
- return true;
- }
- return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort);
- }
-
- static boolean drawTimeUpdate(long millis, int pixels) {
- return sStrategy.drawTimeUpdate(millis, pixels);
- }
-
- static void resetPageState() {
- sStrategy.resetPageState();
- }
-
- static void initPrefs() {
- final String[] prefs = { PREF_DISPLAYPORT_STRATEGY,
- PREF_DISPLAYPORT_FM_MULTIPLIER,
- PREF_DISPLAYPORT_FM_DANGER_X,
- PREF_DISPLAYPORT_FM_DANGER_Y,
- PREF_DISPLAYPORT_VB_MULTIPLIER,
- PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD,
- PREF_DISPLAYPORT_VB_REVERSE_BUFFER,
- PREF_DISPLAYPORT_VB_DANGER_X_BASE,
- PREF_DISPLAYPORT_VB_DANGER_Y_BASE,
- PREF_DISPLAYPORT_VB_DANGER_X_INCR,
- PREF_DISPLAYPORT_VB_DANGER_Y_INCR,
- PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD };
-
- PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
- private final Map<String, Integer> mValues = new HashMap<String, Integer>();
-
- @Override public void prefValue(String pref, int value) {
- mValues.put(pref, value);
- }
-
- @Override public void finish() {
- setStrategy(mValues);
- }
- });
- }
-
- /**
- * Set the active strategy to use.
- * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the
- * mapping between ints and strategies.
- */
- static boolean setStrategy(Map<String, Integer> prefs) {
- Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY);
- if (strategy == null) {
- return false;
- }
-
- switch (strategy) {
- case 0:
- sStrategy = new FixedMarginStrategy(prefs);
- break;
- case 1:
- sStrategy = new VelocityBiasStrategy(prefs);
- break;
- case 2:
- sStrategy = new DynamicResolutionStrategy(prefs);
- break;
- case 3:
- sStrategy = new NoMarginStrategy(prefs);
- break;
- case 4:
- sStrategy = new PredictionBiasStrategy(prefs);
- break;
- default:
- Log.e(LOGTAG, "Invalid strategy index specified");
- return false;
- }
- Log.i(LOGTAG, "Set strategy " + sStrategy.toString());
- return true;
- }
-
- private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
- Integer value = (prefs == null ? null : prefs.get(prefName));
- return (value == null || value < 0 ? defaultValue : value) / 1000f;
- }
-
- private static abstract class DisplayPortStrategy {
- /** Calculates a displayport given a viewport and panning velocity. */
- public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity);
- /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */
- public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort);
- /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */
- public boolean drawTimeUpdate(long millis, int pixels) { return false; }
- /** Reset any page-specific state stored, as the page being displayed has changed. */
- public void resetPageState() {}
- }
-
- /**
- * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the
- * given metrics object. The area in the returned FloatSize may be less than width*height if the page is
- * small, but it will never be larger than width*height.
- * Note that this process may change the relative aspect ratio of the given dimensions.
- */
- private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) {
- // figure out how much of the desired buffer amount we can actually use on the horizontal axis
- float usableWidth = Math.min(width, metrics.getPageWidth());
- // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and
- // use it on the vertical axis
- float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth);
- float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight());
- if (usableHeight < height && usableWidth == width) {
- // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal
- float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight);
- usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth());
- }
- return new FloatSize(usableWidth, usableHeight);
- }
-
- /**
- * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis
- * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then
- * clamped to page bounds and returned.
- */
- private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) {
- // calculate the danger zone amounts in pixels
- float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
- float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
- rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
- // clamp to page bounds
- return clampToPageBounds(rect, metrics);
- }
-
- /**
- * Calculate the display port by expanding the viewport by the specified
- * margins, then clamping to the page size.
- */
- private static DisplayPortMetrics getPageClampedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
- float left = metrics.viewportRectLeft - margins.left;
- float top = metrics.viewportRectTop - margins.top;
- float right = metrics.viewportRectRight() + margins.right;
- float bottom = metrics.viewportRectBottom() + margins.bottom;
- left = Math.max(metrics.pageRectLeft, left);
- top = Math.max(metrics.pageRectTop, top);
- right = Math.min(metrics.pageRectRight, right);
- bottom = Math.min(metrics.pageRectBottom, bottom);
-
- return new DisplayPortMetrics(left, top, right, bottom, zoom);
- }
-
- /**
- * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
- * does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
- * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
- * metrics.getPageWidth(); and the same for the y axis.
- */
- private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) {
- // check how much we're overflowing in each direction. note that at most one of leftOverflow
- // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow
- // can be greater than zero, because of the assumption described in the method javadoc.
- float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left);
- float rightOverflow = (metrics.viewportRectRight() + margins.right) - metrics.pageRectRight;
- float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top);
- float bottomOverflow = (metrics.viewportRectBottom() + margins.bottom) - metrics.pageRectBottom;
-
- // if the margins overflow the page bounds, shift them to other side on the same axis
- if (leftOverflow > 0) {
- margins.left -= leftOverflow;
- margins.right += leftOverflow;
- } else if (rightOverflow > 0) {
- margins.right -= rightOverflow;
- margins.left += rightOverflow;
- }
- if (topOverflow > 0) {
- margins.top -= topOverflow;
- margins.bottom += topOverflow;
- } else if (bottomOverflow > 0) {
- margins.bottom -= bottomOverflow;
- margins.top += bottomOverflow;
- }
- return margins;
- }
-
- /**
- * Clamp the given rect to the page bounds and return it.
- */
- private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) {
- if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop;
- if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft;
- if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight;
- if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom;
- return rect;
- }
-
- /**
- * This class implements the variation where we basically don't bother with a a display port.
- */
- private static class NoMarginStrategy extends DisplayPortStrategy {
- NoMarginStrategy(Map<String, Integer> prefs) {
- // no prefs in this strategy
- }
-
- @Override
- public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
- return new DisplayPortMetrics(metrics.viewportRectLeft,
- metrics.viewportRectTop,
- metrics.viewportRectRight(),
- metrics.viewportRectBottom(),
- metrics.zoomFactor);
- }
-
- @Override
- public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
- return true;
- }
-
- @Override
- public String toString() {
- return "NoMarginStrategy";
- }
- }
-
- /**
- * This class implements the variation where we use a fixed-size margin on the display port.
- * The margin is always 300 pixels in all directions, except when we are (a) approaching a page
- * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain
- * the area of the display port by (a) shifting the buffer to the other side on the same axis,
- * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on
- * one axis.
- */
- private static class FixedMarginStrategy extends DisplayPortStrategy {
- // The length of each axis of the display port will be the corresponding view length
- // multiplied by this factor.
- private final float SIZE_MULTIPLIER;
-
- // If the visible rect is within the danger zone (measured as a fraction of the view size
- // from the edge of the displayport) we start redrawing to minimize checkerboarding.
- private final float DANGER_ZONE_X_MULTIPLIER;
- private final float DANGER_ZONE_Y_MULTIPLIER;
-
- FixedMarginStrategy(Map<String, Integer> prefs) {
- SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000);
- DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100);
- DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200);
- }
-
- @Override
- public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
- float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
- float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
-
- // we need to avoid having a display port that is larger than the page, or we will end up
- // painting things outside the page bounds (bug 729169). we simultaneously need to make
- // the display port as large as possible so that we redraw less. reshape the display
- // port dimensions to accomplish this.
- FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
- float horizontalBuffer = usableSize.width - metrics.getWidth();
- float verticalBuffer = usableSize.height - metrics.getHeight();
-
- // and now calculate the display port margins based on how much buffer we've decided to use and
- // the page bounds, ensuring we use all of the available buffer amounts on one side or the other
- // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is
- // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer
- // is split).
- RectF margins = new RectF();
- margins.left = horizontalBuffer / 2.0f;
- margins.right = horizontalBuffer - margins.left;
- margins.top = verticalBuffer / 2.0f;
- margins.bottom = verticalBuffer - margins.top;
- margins = shiftMarginsForPageBounds(margins, metrics);
-
- return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
- }
-
- @Override
- public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
- // Increase the size of the viewport based on the danger zone multiplier (and clamp to page
- // boundaries), and intersect it with the current displayport to determine whether we're
- // close to checkerboarding.
- RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
- return !displayPort.contains(adjustedViewport);
- }
-
- @Override
- public String toString() {
- return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER;
- }
- }
-
- /**
- * This class implements the variation with a small fixed-size margin with velocity bias.
- * In this variation, the default margins are pretty small relative to the view size, but
- * they are affected by the panning velocity. Specifically, if we are panning on one axis,
- * we remove the margins on the other axis because we are likely axis-locked. Also once
- * we are panning in one direction above a certain threshold velocity, we shift the buffer
- * so that it is almost entirely in the direction of the pan, with a little bit in the
- * reverse direction.
- */
- private static class VelocityBiasStrategy extends DisplayPortStrategy {
- // The length of each axis of the display port will be the corresponding view length
- // multiplied by this factor.
- private final float SIZE_MULTIPLIER;
- // The velocity above which we apply the velocity bias
- private final float VELOCITY_THRESHOLD;
- // How much of the buffer to keep in the reverse direction of the velocity
- private final float REVERSE_BUFFER;
- // If the visible rect is within the danger zone we start redrawing to minimize
- // checkerboarding. the danger zone amount is a linear function of the form:
- // viewportsize * (base + velocity * incr)
- // where base and incr are configurable values.
- private final float DANGER_ZONE_BASE_X_MULTIPLIER;
- private final float DANGER_ZONE_BASE_Y_MULTIPLIER;
- private final float DANGER_ZONE_INCR_X_MULTIPLIER;
- private final float DANGER_ZONE_INCR_Y_MULTIPLIER;
-
- VelocityBiasStrategy(Map<String, Integer> prefs) {
- SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000);
- VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32);
- REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200);
- DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000);
- DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000);
- DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0);
- DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0);
- }
-
- /**
- * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values.
- * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER
- * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the
- * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction
- * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the
- * two margins on that axis.
- */
- private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) {
- RectF margins = new RectF();
-
- if (velocity.x > VELOCITY_THRESHOLD) {
- margins.left = xAmount * REVERSE_BUFFER;
- } else if (velocity.x < -VELOCITY_THRESHOLD) {
- margins.left = xAmount * (1.0f - REVERSE_BUFFER);
- } else {
- margins.left = xAmount / 2.0f;
- }
- margins.right = xAmount - margins.left;
-
- if (velocity.y > VELOCITY_THRESHOLD) {
- margins.top = yAmount * REVERSE_BUFFER;
- } else if (velocity.y < -VELOCITY_THRESHOLD) {
- margins.top = yAmount * (1.0f - REVERSE_BUFFER);
- } else {
- margins.top = yAmount / 2.0f;
- }
- margins.bottom = yAmount - margins.top;
-
- return margins;
- }
-
- @Override
- public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
- float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
- float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
-
- // but if we're panning on one axis, set the margins for the other axis to zero since we are likely
- // axis locked and won't be displaying that extra area.
- if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) {
- displayPortHeight = metrics.getHeight();
- } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) {
- displayPortWidth = metrics.getWidth();
- }
-
- // we need to avoid having a display port that is larger than the page, or we will end up
- // painting things outside the page bounds (bug 729169).
- displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth());
- displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight());
- float horizontalBuffer = displayPortWidth - metrics.getWidth();
- float verticalBuffer = displayPortHeight - metrics.getHeight();
-
- // split the buffer amounts into margins based on velocity, and shift it to
- // take into account the page bounds
- RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity);
- margins = shiftMarginsForPageBounds(margins, metrics);
-
- return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
- }
-
- @Override
- public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
- // calculate the danger zone amounts based on the prefs
- float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER));
- float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER));
- // clamp it such that when added to the viewport, they don't exceed page size.
- // this is a prerequisite to calling shiftMarginsForPageBounds as we do below.
- dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth());
- dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight());
-
- // split the danger zone into margins based on velocity, and ensure it doesn't exceed
- // page bounds.
- RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity);
- dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics);
-
- // we're about to checkerboard if the current viewport area + the danger zone margins
- // fall out of the current displayport anywhere.
- RectF adjustedViewport = new RectF(
- metrics.viewportRectLeft - dangerMargins.left,
- metrics.viewportRectTop - dangerMargins.top,
- metrics.viewportRectRight() + dangerMargins.right,
- metrics.viewportRectBottom() + dangerMargins.bottom);
- return !displayPort.contains(adjustedViewport);
- }
-
- @Override
- public String toString() {
- return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER
- + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER
- + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER;
- }
- }
-
- /**
- * This class implements the variation where we draw more of the page at low resolution while panning.
- * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw
- * resolution to compensate. This results in the same device-pixel area drawn; the compositor then
- * scales this up to the viewport zoom level. This results in a large area of the page drawn but it
- * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding,
- * where we draw less but never even show it on the screen.
- */
- private static class DynamicResolutionStrategy extends DisplayPortStrategy {
- // The length of each axis of the display port will be the corresponding view length
- // multiplied by this factor.
- private static final float SIZE_MULTIPLIER = 1.5f;
-
- // The velocity above which we start zooming out the display port to keep up
- // with the panning.
- private static final float VELOCITY_EXPANSION_THRESHOLD = GeckoAppShell.getDpi() / 16f;
-
- // How much we increase the display port based on velocity. Assuming no friction and
- // splitting (see below), this should be be the number of frames (@60fps) between us
- // calculating the display port and the draw of the *next* display port getting composited
- // and displayed on the screen. This is because the timeline looks like this:
- // Java: pan pan pan pan pan pan ! pan pan pan pan pan pan !
- // Gecko: \-> draw -> composite / \-> draw -> composite /
- // The display port calculated on the first "pan" gets composited to the screen at the
- // first exclamation mark, and remains on the screen until the second exclamation mark.
- // In order to avoid checkerboarding, that display port must be able to contain all of
- // the panning until the second exclamation mark, which encompasses two entire draw/composite
- // cycles.
- // If we take into account friction, our velocity multiplier should be reduced as the
- // amount of pan will decrease each time. If we take into account display port splitting,
- // it should be increased as the splitting means some of the display port will be used to
- // draw in the opposite direction of the velocity. For now I'm assuming these two cancel
- // each other out.
- private static final float VELOCITY_MULTIPLIER = 60.0f;
-
- // The following constants adjust how biased the display port is in the direction of panning.
- // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the
- // display port "buffer" area, otherwise we use the slow split factor. This is based on the
- // assumption that if the user is panning fast, they are less likely to reverse directions
- // and go backwards, so we should spend more of our display port buffer in the direction of
- // panning.
- private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f;
- private static final float FAST_SPLIT_FACTOR = 0.95f;
- private static final float SLOW_SPLIT_FACTOR = 0.8f;
-
- // The following constants are used for viewport prediction; we use them to estimate where
- // the viewport will be soon and whether or not we should trigger a draw right now. "soon"
- // in the previous sentence really refers to the amount of time it would take to draw and
- // composite from the point at which we do the calculation, and that is not really a known
- // quantity. The velocity multiplier is how much we multiply the velocity by; it has the
- // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account
- // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the
- // viewport size that we use as an extra "danger zone" around the viewport; if this danger
- // zone falls outside the display port then we are approaching the point at which we will
- // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is
- // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the
- // danger zone, and thus will be constantly drawing.
- private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f;
- private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f)
-
- DynamicResolutionStrategy(Map<String, Integer> prefs) {
- // ignore prefs for now
- }
-
- @Override
- public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
- float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
- float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
-
- // for resolution calculation purposes, we need to know what the adjusted display port dimensions
- // would be if we had zero velocity, so calculate that here before we increase the display port
- // based on velocity.
- FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
-
- // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their
- // relative aspect ratio.
- if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) {
- float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth,
- Math.abs(velocity.y) / displayPortHeight);
- velocityFactor *= VELOCITY_MULTIPLIER;
-
- displayPortWidth += (displayPortWidth * velocityFactor);
- displayPortHeight += (displayPortHeight * velocityFactor);
- }
-
- // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels)
- // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied
- // by metrics.zoomFactor
-
- // we need to avoid having a display port that is larger than the page, or we will end up
- // painting things outside the page bounds (bug 729169). we simultaneously need to make
- // the display port as large as possible so that we redraw less. reshape the display
- // port dimensions to accomplish this. this may change the aspect ratio of the display port,
- // but we are assuming that this is desirable because the advantages from pre-drawing will
- // outweigh the disadvantages from any buffer reallocations that might occur.
- FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
- float horizontalBuffer = usableSize.width - metrics.getWidth();
- float verticalBuffer = usableSize.height - metrics.getHeight();
-
- // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have.
- // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case
- // the user scrolls there. we now need to split the buffer area on each axis so that we know
- // what the exact margins on each side will be. first we split the buffer amount based on the direction
- // we're moving, so that we have a larger buffer in the direction of travel.
- RectF margins = new RectF();
- margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x);
- margins.right = horizontalBuffer - margins.left;
- margins.top = splitBufferByVelocity(verticalBuffer, velocity.y);
- margins.bottom = verticalBuffer - margins.top;
-
- // then, we account for running into the page bounds - so that if we hit the top of the page, we need
- // to drop the top margin and move that amount to the bottom margin.
- margins = shiftMarginsForPageBounds(margins, metrics);
-
- // finally, we calculate the resolution we want to render the display port area at. We do this
- // so that as we expand the display port area (because of velocity), we reduce the resolution of
- // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate
- // the reduction in resolution by comparing the display port size with and without the velocity
- // changes applied.
- // this effectively means that as we pan faster and faster, the display port grows, but we paint
- // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing
- // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone.
- // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we
- // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor.
- float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height);
- float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor);
-
- DisplayPortMetrics dpMetrics = new DisplayPortMetrics(
- metrics.viewportRectLeft - margins.left,
- metrics.viewportRectTop - margins.top,
- metrics.viewportRectRight() + margins.right,
- metrics.viewportRectBottom() + margins.bottom,
- displayResolution);
- return dpMetrics;
- }
-
- /**
- * Split the given buffer amount into two based on the velocity.
- * Given an amount of total usable buffer on an axis, this will
- * return the amount that should be used on the left/top side of
- * the axis (the side which a negative velocity vector corresponds
- * to).
- */
- private float splitBufferByVelocity(float amount, float velocity) {
- // if no velocity, so split evenly
- if (FloatUtils.fuzzyEquals(velocity, 0)) {
- return amount / 2.0f;
- }
- // if we're moving quickly, assign more of the amount in that direction
- // since is is less likely that we will reverse direction immediately
- if (velocity < -VELOCITY_FAST_THRESHOLD) {
- return amount * FAST_SPLIT_FACTOR;
- }
- if (velocity > VELOCITY_FAST_THRESHOLD) {
- return amount * (1.0f - FAST_SPLIT_FACTOR);
- }
- // if we're moving slowly, then assign less of the amount in that direction
- if (velocity < 0) {
- return amount * SLOW_SPLIT_FACTOR;
- } else {
- return amount * (1.0f - SLOW_SPLIT_FACTOR);
- }
- }
-
- @Override
- public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
- // Expand the viewport based on our velocity (and clamp it to page boundaries).
- // Then intersect it with the last-requested displayport to determine whether we're
- // close to checkerboarding.
-
- RectF predictedViewport = metrics.getViewport();
-
- // first we expand the viewport in the direction we're moving based on some
- // multiple of the current velocity.
- if (velocity.length() > 0) {
- if (velocity.x < 0) {
- predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
- } else if (velocity.x > 0) {
- predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
- }
-
- if (velocity.y < 0) {
- predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
- } else if (velocity.y > 0) {
- predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
- }
- }
-
- // then we expand the viewport evenly in all directions just to have an extra
- // safety zone. this also clamps it to page bounds.
- predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics);
- return !displayPort.contains(predictedViewport);
- }
-
- @Override
- public String toString() {
- return "DynamicResolutionStrategy";
- }
- }
-
- /**
- * This class implements the variation where we use the draw time to predict where we will be when
- * a draw completes, and draw that instead of where we are now. In this variation, when our panning
- * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can
- * pan in any direction without encountering checkerboarding.
- * Once the user is panning, we modify the displayport to encompass an area range of where we think
- * the user will be when the draw completes. This heuristic relies on both the estimated draw time
- * the panning velocity; unexpected changes in either of these values will cause the heuristic to
- * fail and show checkerboard.
- */
- private static class PredictionBiasStrategy extends DisplayPortStrategy {
- private static float VELOCITY_THRESHOLD;
-
- private int mPixelArea; // area of the viewport, used in draw time calculations
- private int mMinFramesToDraw; // minimum number of frames we take to draw
- private int mMaxFramesToDraw; // maximum number of frames we take to draw
-
- PredictionBiasStrategy(Map<String, Integer> prefs) {
- VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16);
- resetPageState();
- }
-
- @Override
- public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
- float width = metrics.getWidth();
- float height = metrics.getHeight();
- mPixelArea = (int)(width * height);
-
- if (velocity.length() < VELOCITY_THRESHOLD) {
- // if we're going slow, expand the displayport to 9x viewport size
- RectF margins = new RectF(width, height, width, height);
- return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
- }
-
- // figure out how far we expect to be
- float minDx = velocity.x * mMinFramesToDraw;
- float minDy = velocity.y * mMinFramesToDraw;
- float maxDx = velocity.x * mMaxFramesToDraw;
- float maxDy = velocity.y * mMaxFramesToDraw;
-
- // figure out how many pixels we will be drawing when we draw the above-calculated range.
- // this will be larger than the viewport area.
- float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy));
- // adjust how far we will get because of the time spent drawing all these extra pixels. this
- // will again increase the number of pixels drawn so really we could keep iterating this over
- // and over, but once seems enough for now.
- maxDx = maxDx * pixelsToDraw / mPixelArea;
- maxDy = maxDy * pixelsToDraw / mPixelArea;
-
- // and finally generate the displayport. the min/max stuff takes care of
- // negative velocities as well as positive.
- RectF margins = new RectF(
- -Math.min(minDx, maxDx),
- -Math.min(minDy, maxDy),
- Math.max(minDx, maxDx),
- Math.max(minDy, maxDy));
- return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
- }
-
- @Override
- public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
- // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs.
- // refer to the comments in calculate() to understand what this is doing.
- float minDx = velocity.x * mMinFramesToDraw;
- float minDy = velocity.y * mMinFramesToDraw;
- float maxDx = velocity.x * mMaxFramesToDraw;
- float maxDy = velocity.y * mMaxFramesToDraw;
- float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy));
- maxDx = maxDx * pixelsToDraw / mPixelArea;
- maxDy = maxDy * pixelsToDraw / mPixelArea;
-
- // now that we have an idea of how far we will be when the draw completes, take the farthest
- // end of that range and see if it falls outside the displayport bounds. if it does, allow
- // the draw to go through
- RectF predictedViewport = metrics.getViewport();
- predictedViewport.left += maxDx;
- predictedViewport.top += maxDy;
- predictedViewport.right += maxDx;
- predictedViewport.bottom += maxDy;
-
- predictedViewport = clampToPageBounds(predictedViewport, metrics);
- return !displayPort.contains(predictedViewport);
- }
-
- @Override
- public boolean drawTimeUpdate(long millis, int pixels) {
- // calculate the number of frames it took to draw a viewport-sized area
- float normalizedTime = (float)mPixelArea * millis / pixels;
- int normalizedFrames = (int) Math.ceil(normalizedTime * 60f / 1000f);
- // broaden our range on how long it takes to draw if the draw falls outside
- // the range. this allows it to grow gradually. this heuristic may need to
- // be tweaked into more of a floating window average or something.
- if (normalizedFrames <= mMinFramesToDraw) {
- mMinFramesToDraw--;
- } else if (normalizedFrames > mMaxFramesToDraw) {
- mMaxFramesToDraw++;
- } else {
- return true;
- }
- Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]");
- return true;
- }
-
- @Override
- public void resetPageState() {
- mMinFramesToDraw = 0;
- mMaxFramesToDraw = 2;
- }
-
- @Override
- public String toString() {
- return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD;
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.util.FloatUtils;
-
-import android.graphics.RectF;
-
-/*
- * This class keeps track of the area we request Gecko to paint, as well
- * as the resolution of the paint. The area may be different from the visible
- * area of the page, and the resolution may be different from the resolution
- * used in the compositor to render the page. This is so that we can ask Gecko
- * to paint a much larger area without using extra memory, and then render some
- * subsection of that with compositor scaling.
- */
-public final class DisplayPortMetrics {
- @WrapForJNI
- public final float resolution;
- @WrapForJNI
- private final RectF mPosition;
-
- public DisplayPortMetrics() {
- this(0, 0, 0, 0, 1);
- }
-
- @WrapForJNI
- public DisplayPortMetrics(float left, float top, float right, float bottom, float resolution) {
- this.resolution = resolution;
- mPosition = new RectF(left, top, right, bottom);
- }
-
- public float getLeft() {
- return mPosition.left;
- }
-
- public float getTop() {
- return mPosition.top;
- }
-
- public float getRight() {
- return mPosition.right;
- }
-
- public float getBottom() {
- return mPosition.bottom;
- }
-
- public boolean contains(RectF rect) {
- return mPosition.contains(rect);
- }
-
- public boolean fuzzyEquals(DisplayPortMetrics metrics) {
- return RectUtils.fuzzyEquals(mPosition, metrics.mPosition)
- && FloatUtils.fuzzyEquals(resolution, metrics.resolution);
- }
-
- public String toJSON() {
- StringBuilder sb = new StringBuilder(256);
- sb.append("{ \"left\": ").append(mPosition.left)
- .append(", \"top\": ").append(mPosition.top)
- .append(", \"right\": ").append(mPosition.right)
- .append(", \"bottom\": ").append(mPosition.bottom)
- .append(", \"resolution\": ").append(resolution)
- .append('}');
- return sb.toString();
- }
-
- @Override
- public String toString() {
- return "DisplayPortMetrics v=(" + mPosition.left + "," + mPosition.top + "," + mPosition.right + ","
- + mPosition.bottom + ") z=" + resolution;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/DrawTimingQueue.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.os.SystemClock;
-
-/**
- * A custom-built data structure to assist with measuring draw times.
- *
- * This class maintains a fixed-size circular buffer of DisplayPortMetrics
- * objects and associated timestamps. It provides only three operations, which
- * is all we require for our purposes of measuring draw times. Note
- * in particular that the class is designed so that even though it is
- * accessed from multiple threads, it does not require synchronization;
- * any concurrency errors that result from this are handled gracefully.
- *
- * Assuming an unrolled buffer so that mTail is greater than mHead, the data
- * stored in the buffer at entries [mHead, mTail) will never be modified, and
- * so are "safe" to read. If this reading is done on the same thread that
- * owns mHead, then reading the range [mHead, mTail) is guaranteed to be safe
- * since the range itself will not shrink.
- */
-final class DrawTimingQueue {
- private static final String LOGTAG = "GeckoDrawTimingQueue";
- private static final int BUFFER_SIZE = 16;
-
- private final DisplayPortMetrics[] mMetrics;
- private final long[] mTimestamps;
-
- private int mHead;
- private int mTail;
-
- DrawTimingQueue() {
- mMetrics = new DisplayPortMetrics[BUFFER_SIZE];
- mTimestamps = new long[BUFFER_SIZE];
- mHead = BUFFER_SIZE - 1;
- }
-
- /**
- * Add a new entry to the tail of the queue. If the buffer is full,
- * do nothing. This must only be called from the Java UI thread.
- */
- boolean add(DisplayPortMetrics metrics) {
- if (mHead == mTail) {
- return false;
- }
- mMetrics[mTail] = metrics;
- mTimestamps[mTail] = SystemClock.uptimeMillis();
- mTail = (mTail + 1) % BUFFER_SIZE;
- return true;
- }
-
- /**
- * Find the timestamp associated with the given metrics, AND remove
- * all metrics objects from the start of the queue up to and including
- * the one provided. Note that because of draw coalescing, the metrics
- * object passed in here may not be the one at the head of the queue,
- * and so we must iterate our way through the list to find it.
- * This must only be called from the compositor thread.
- */
- long findTimeFor(DisplayPortMetrics metrics) {
- // keep a copy of the tail pointer so that we ignore new items
- // added to the queue while we are searching. this is fine because
- // the one we are looking for will either have been added already
- // or will not be in the queue at all.
- int tail = mTail;
- // walk through the "safe" range from mHead to tail; these entries
- // will not be modified unless we change mHead.
- int i = (mHead + 1) % BUFFER_SIZE;
- while (i != tail) {
- if (mMetrics[i].fuzzyEquals(metrics)) {
- // found it, copy out the timestamp to a local var BEFORE
- // changing mHead or add could clobber the timestamp.
- long timestamp = mTimestamps[i];
- mHead = i;
- return timestamp;
- }
- i = (i + 1) % BUFFER_SIZE;
- }
- return -1;
- }
-
- /**
- * Reset the buffer to empty.
- * This must only be called from the compositor thread.
- */
- void reset() {
- // we can only modify mHead on this thread.
- mHead = (mTail + BUFFER_SIZE - 1) % BUFFER_SIZE;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
+++ /dev/null
@@ -1,605 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.util.FloatUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.graphics.PointF;
-import android.support.v4.view.ViewCompat;
-import android.util.Log;
-import android.view.animation.DecelerateInterpolator;
-import android.view.MotionEvent;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Set;
-
-public class DynamicToolbarAnimator {
- private static final String LOGTAG = "GeckoDynamicToolbarAnimator";
- private static final String PREF_SCROLL_TOOLBAR_THRESHOLD = "browser.ui.scroll-toolbar-threshold";
-
- public static enum PinReason {
- RELAYOUT,
- ACTION_MODE,
- FULL_SCREEN,
- CARET_DRAG
- }
-
- private final Set<PinReason> pinFlags = Collections.synchronizedSet(EnumSet.noneOf(PinReason.class));
-
- // The duration of the animation in ns
- private static final long ANIMATION_DURATION = 250000000;
-
- private final GeckoLayerClient mTarget;
- private final List<LayerView.DynamicToolbarListener> mListeners;
-
- /* The translation to be applied to the toolbar UI view. This is the
- * distance from the default/initial location (at the top of the screen,
- * visible to the user) to where we want it to be. This variable should
- * always be between 0 (toolbar fully visible) and the height of the toolbar
- * (toolbar fully hidden), inclusive.
- */
- private float mToolbarTranslation;
-
- /* The translation to be applied to the LayerView. This is the distance from
- * the default/initial location (just below the toolbar, with the bottom
- * extending past the bottom of the screen) to where we want it to be.
- * This variable should always be between 0 and the height of the toolbar,
- * inclusive.
- */
- private float mLayerViewTranslation;
-
- /* This stores the maximum translation that can be applied to the toolbar
- * and layerview when scrolling. This is populated with the height of the
- * toolbar. */
- private float mMaxTranslation;
-
- /* This interpolator is used for the above mentioned animation */
- private DecelerateInterpolator mInterpolator;
-
- /* This is the proportion of the viewport rect that needs to be travelled
- * while scrolling before the translation will start taking effect.
- */
- private float SCROLL_TOOLBAR_THRESHOLD = 0.20f;
- /* The ID of the prefs listener for the scroll-toolbar threshold */
- private final PrefsHelper.PrefHandler mPrefObserver;
-
- /* While we are resizing the viewport to account for the toolbar, the Java
- * code and painted layer metrics in the compositor have different notions
- * of the CSS viewport height. The Java value is stored in the
- * GeckoLayerClient's viewport metrics, and the Gecko one is stored here.
- * This allows us to adjust fixed-pos items correctly.
- * You must synchronize on mTarget.getLock() to read/write this. */
- private Integer mHeightDuringResize;
-
- /* This tracks if we should trigger a "snap" on the next composite. A "snap"
- * is when we simultaneously move the LayerView and change the scroll offset
- * in the compositor so that everything looks the same on the screen but
- * has really been shifted.
- * You must synchronize on |this| to read/write this. */
- private boolean mSnapRequired = false;
-
- /* The task that handles showing/hiding toolbar */
- private DynamicToolbarAnimationTask mAnimationTask;
-
- /* The start point of a drag, used for scroll-based dynamic toolbar
- * behaviour. */
- private PointF mTouchStart;
- private float mLastTouch;
-
- /* Set to true when root content is being scrolled */
- private boolean mScrollingRootContent;
-
- public DynamicToolbarAnimator(GeckoLayerClient aTarget) {
- mTarget = aTarget;
- mListeners = new ArrayList<LayerView.DynamicToolbarListener>();
-
- mInterpolator = new DecelerateInterpolator();
-
- // Listen to the dynamic toolbar pref
- mPrefObserver = new PrefsHelper.PrefHandlerBase() {
- @Override
- public void prefValue(String pref, int value) {
- SCROLL_TOOLBAR_THRESHOLD = value / 100.0f;
- }
- };
- PrefsHelper.addObserver(new String[] { PREF_SCROLL_TOOLBAR_THRESHOLD }, mPrefObserver);
-
- // JPZ doesn't notify when scrolling root content. This maintains existing behaviour.
- if (!AppConstants.MOZ_ANDROID_APZ) {
- mScrollingRootContent = true;
- }
- }
-
- public void destroy() {
- PrefsHelper.removeObserver(mPrefObserver);
- }
-
- public void addTranslationListener(LayerView.DynamicToolbarListener aListener) {
- mListeners.add(aListener);
- }
-
- public void removeTranslationListener(LayerView.DynamicToolbarListener aListener) {
- mListeners.remove(aListener);
- }
-
- private void fireListeners() {
- for (LayerView.DynamicToolbarListener listener : mListeners) {
- listener.onTranslationChanged(mToolbarTranslation, mLayerViewTranslation);
- }
- }
-
- void onPanZoomStopped() {
- for (LayerView.DynamicToolbarListener listener : mListeners) {
- listener.onPanZoomStopped();
- }
- }
-
- void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
- for (LayerView.DynamicToolbarListener listener : mListeners) {
- listener.onMetricsChanged(aMetrics);
- }
- }
-
- public void setMaxTranslation(float maxTranslation) {
- ThreadUtils.assertOnUiThread();
- if (maxTranslation < 0) {
- Log.e(LOGTAG, "Got a negative max-translation value: " + maxTranslation + "; clamping to zero");
- mMaxTranslation = 0;
- } else {
- mMaxTranslation = maxTranslation;
- }
- }
-
- public float getMaxTranslation() {
- return mMaxTranslation;
- }
-
- public float getToolbarTranslation() {
- return mToolbarTranslation;
- }
-
- /**
- * If true, scroll changes will not affect translation.
- */
- public boolean isPinned() {
- return !pinFlags.isEmpty();
- }
-
- public boolean isPinnedBy(PinReason reason) {
- return pinFlags.contains(reason);
- }
-
- public void setPinned(boolean pinned, PinReason reason) {
- if (pinned) {
- pinFlags.add(reason);
- } else {
- pinFlags.remove(reason);
- }
- }
-
- public void showToolbar(boolean immediately) {
- animateToolbar(true, immediately);
- }
-
- public void hideToolbar(boolean immediately) {
- animateToolbar(false, immediately);
- }
-
- public void setScrollingRootContent(boolean isRootContent) {
- mScrollingRootContent = isRootContent;
- }
-
- private void animateToolbar(final boolean showToolbar, boolean immediately) {
- ThreadUtils.assertOnUiThread();
-
- if (mAnimationTask != null) {
- mTarget.getView().removeRenderTask(mAnimationTask);
- mAnimationTask = null;
- }
-
- float desiredTranslation = (showToolbar ? 0 : mMaxTranslation);
- Log.v(LOGTAG, "Requested " + (immediately ? "immediate " : "") + "toolbar animation to translation " + desiredTranslation);
- if (FloatUtils.fuzzyEquals(mToolbarTranslation, desiredTranslation)) {
- // If we're already pretty much in the desired position, don't bother
- // with a full animation; do an immediate jump
- immediately = true;
- Log.v(LOGTAG, "Changing animation to immediate jump");
- }
-
- if (showToolbar && immediately) {
- // Special case for showing the toolbar immediately: some of the call
- // sites expect this to happen synchronously, so let's do that. This
- // is safe because if we are showing the toolbar from a hidden state
- // there is no chance of showing garbage
- mToolbarTranslation = desiredTranslation;
- fireListeners();
- // And then proceed with the normal flow (some of which will be
- // a no-op now)...
- }
-
- if (!showToolbar) {
- // If we are hiding the toolbar, we need to move the LayerView first,
- // so that we don't end up showing garbage under the toolbar when
- // it is hidden. In the case that we are showing the toolbar, we
- // move the LayerView after the toolbar is shown - the
- // DynamicToolbarAnimationTask calls that upon completion.
- shiftLayerView(desiredTranslation);
- }
-
- mAnimationTask = new DynamicToolbarAnimationTask(desiredTranslation, immediately, showToolbar);
- mTarget.getView().postRenderTask(mAnimationTask);
- }
-
- private synchronized void shiftLayerView(float desiredTranslation) {
- float layerViewTranslationNeeded = desiredTranslation - mLayerViewTranslation;
- mLayerViewTranslation = desiredTranslation;
- synchronized (mTarget.getLock()) {
- mHeightDuringResize = new Integer(mTarget.getViewportMetrics().viewportRectHeight);
- mSnapRequired = mTarget.setViewportSize(
- mTarget.getView().getWidth(),
- mTarget.getView().getHeight() - Math.round(mMaxTranslation - mLayerViewTranslation),
- new PointF(0, -layerViewTranslationNeeded));
- if (!mSnapRequired) {
- mHeightDuringResize = null;
- ThreadUtils.postToUiThread(new Runnable() {
- // Post to run it outside of the synchronize blocks. The
- // delay shouldn't hurt.
- @Override
- public void run() {
- fireListeners();
- }
- });
- }
- // Request a composite, which will trigger the snap.
- mTarget.getView().requestRender();
- }
- }
-
- IntSize getViewportSize() {
- ThreadUtils.assertOnUiThread();
-
- int viewWidth = mTarget.getView().getWidth();
- int viewHeight = mTarget.getView().getHeight();
- float toolbarTranslation = mToolbarTranslation;
- if (mAnimationTask != null) {
- // If we have an animation going, mToolbarTranslation may be in flux
- // and we should use the final value it will settle on.
- toolbarTranslation = mAnimationTask.getFinalToolbarTranslation();
- }
- int viewHeightVisible = viewHeight - Math.round(mMaxTranslation - toolbarTranslation);
- return new IntSize(viewWidth, viewHeightVisible);
- }
-
- boolean isResizing() {
- return mHeightDuringResize != null;
- }
-
- private final Runnable mSnapRunnable = new Runnable() {
- private int mFrame = 0;
-
- @Override
- public final void run() {
- // It takes 2 frames for the view translation to take effect, at
- // least on a Nexus 4 device running Android 4.2.2. So we wait for
- // two frames before doing the notifyAll(), otherwise we get a
- // short user-visible glitch.
- // TODO: find a better way to do this, if possible.
- if (mFrame == 1) {
- synchronized (this) {
- this.notifyAll();
- }
- mFrame = 0;
- return;
- }
-
- if (mFrame == 0) {
- fireListeners();
- }
-
- ViewCompat.postOnAnimation(mTarget.getView(), this);
- mFrame++;
- }
- };
-
- void scrollChangeResizeCompleted() {
- synchronized (mTarget.getLock()) {
- Log.v(LOGTAG, "Scrollchange resize completed");
- mHeightDuringResize = null;
- }
- }
-
- /**
- * "Shrinks" the absolute value of aValue by moving it closer to zero by
- * aShrinkAmount, but prevents it from crossing over zero. If aShrinkAmount
- * is negative it is ignored.
- * @return The shrunken value.
- */
- private static float shrinkAbs(float aValue, float aShrinkAmount) {
- if (aShrinkAmount <= 0) {
- return aValue;
- }
- float shrinkBy = Math.min(Math.abs(aValue), aShrinkAmount);
- return (aValue < 0 ? aValue + shrinkBy : aValue - shrinkBy);
- }
-
- /**
- * This function takes in a scroll amount and decides how much of that
- * should be used up to translate things on screen because of the dynamic
- * toolbar behaviour. It returns the maximum amount that could be used
- * for translation purposes; the rest must be used for scrolling.
- */
- private float decideTranslation(float aDelta,
- ImmutableViewportMetrics aMetrics,
- float aTouchTravelDistance) {
-
- float exposeThreshold = aMetrics.getHeight() * SCROLL_TOOLBAR_THRESHOLD;
- float translation = aDelta;
-
- if (translation < 0) { // finger moving upwards
- translation = shrinkAbs(translation, aMetrics.getOverscroll().top);
-
- // If the toolbar is in a state between fully hidden and fully shown
- // (i.e. the user is actively translating it), then we want the
- // translation to take effect right away. Or if the user has moved
- // their finger past the required threshold (and is not trying to
- // scroll past the bottom of the page) then also we want the touch
- // to cause translation. If the toolbar is fully visible, we only
- // want the toolbar to hide if the user is scrolling the root content.
- boolean inBetween = (mToolbarTranslation != 0 && mToolbarTranslation != mMaxTranslation);
- boolean reachedThreshold = -aTouchTravelDistance >= exposeThreshold;
- boolean atBottomOfPage = aMetrics.viewportRectBottom() >= aMetrics.pageRectBottom;
- if (inBetween || (mScrollingRootContent && reachedThreshold && !atBottomOfPage)) {
- return translation;
- }
- } else { // finger moving downwards
- translation = shrinkAbs(translation, aMetrics.getOverscroll().bottom);
-
- // Ditto above comment, but in this case if they reached the top and
- // the toolbar is not shown, then we do want to allow translation
- // right away.
- boolean inBetween = (mToolbarTranslation != 0 && mToolbarTranslation != mMaxTranslation);
- boolean reachedThreshold = aTouchTravelDistance >= exposeThreshold;
- boolean atTopOfPage = aMetrics.viewportRectTop <= aMetrics.pageRectTop;
- boolean isToolbarTranslated = (mToolbarTranslation != 0);
- if (inBetween || reachedThreshold || (atTopOfPage && isToolbarTranslated)) {
- return translation;
- }
- }
-
- return 0;
- }
-
- // Timestamp of the start of the touch event used to calculate toolbar velocity
- private long mLastEventTime;
- // Current velocity of the toolbar. Used to populate the velocity queue in C++APZ.
- private float mVelocity;
-
- boolean onInterceptTouchEvent(MotionEvent event) {
- if (isPinned()) {
- return false;
- }
-
- // Animations should never co-exist with the user touching the screen.
- if (mAnimationTask != null) {
- mTarget.getView().removeRenderTask(mAnimationTask);
- mAnimationTask = null;
- }
-
- // we only care about single-finger drags here; any other kind of event
- // should reset and cause us to start over.
- if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE ||
- event.getActionMasked() != MotionEvent.ACTION_MOVE ||
- event.getPointerCount() != 1)
- {
- if (mTouchStart != null) {
- Log.v(LOGTAG, "Resetting touch sequence due to non-move");
- mTouchStart = null;
- mVelocity = 0.0f;
- }
-
- if (event.getActionMasked() == MotionEvent.ACTION_UP) {
- // We need to do this even if the toolbar is already fully
- // visible or fully hidden, because this is what triggers the
- // viewport resize in content and updates the viewport metrics.
- boolean toolbarMostlyVisible = mToolbarTranslation < (mMaxTranslation / 2);
- Log.v(LOGTAG, "All fingers lifted, completing " + (toolbarMostlyVisible ? "show" : "hide"));
- animateToolbar(toolbarMostlyVisible, false);
- }
- return false;
- }
-
- if (mTouchStart != null) {
- float prevDir = mLastTouch - mTouchStart.y;
- float newDir = event.getRawY() - mLastTouch;
- if (prevDir != 0 && newDir != 0 && ((prevDir < 0) != (newDir < 0))) {
- // If the direction of movement changed, reset the travel
- // distance properties.
- mTouchStart = null;
- mVelocity = 0.0f;
- }
- }
-
- if (mTouchStart == null) {
- mTouchStart = new PointF(event.getRawX(), event.getRawY());
- mLastTouch = event.getRawY();
- mLastEventTime = event.getEventTime();
- return false;
- }
-
- float deltaY = event.getRawY() - mLastTouch;
- long currentTime = event.getEventTime();
- float deltaTime = (float)(currentTime - mLastEventTime);
- mLastEventTime = currentTime;
- if (deltaTime > 0.0f) {
- mVelocity = -deltaY / deltaTime;
- } else {
- mVelocity = 0.0f;
- }
- mLastTouch = event.getRawY();
- float travelDistance = event.getRawY() - mTouchStart.y;
-
- ImmutableViewportMetrics metrics = mTarget.getViewportMetrics();
-
- if (metrics.getPageHeight() <= mTarget.getView().getHeight() &&
- mToolbarTranslation == 0) {
- // If the page is short and the toolbar is already visible, don't
- // allow translating it out of view.
- return false;
- }
-
- float translation = decideTranslation(deltaY, metrics, travelDistance);
-
- float oldToolbarTranslation = mToolbarTranslation;
- float oldLayerViewTranslation = mLayerViewTranslation;
- mToolbarTranslation = FloatUtils.clamp(mToolbarTranslation - translation, 0, mMaxTranslation);
- mLayerViewTranslation = FloatUtils.clamp(mLayerViewTranslation - translation, 0, mMaxTranslation);
-
- if (oldToolbarTranslation == mToolbarTranslation &&
- oldLayerViewTranslation == mLayerViewTranslation) {
- return false;
- }
-
- if (mToolbarTranslation == mMaxTranslation) {
- Log.v(LOGTAG, "Toolbar at maximum translation, calling shiftLayerView(" + mMaxTranslation + ")");
- shiftLayerView(mMaxTranslation);
- } else if (mToolbarTranslation == 0) {
- Log.v(LOGTAG, "Toolbar at minimum translation, calling shiftLayerView(0)");
- shiftLayerView(0);
- }
-
- fireListeners();
- mTarget.getView().requestRender();
- return true;
- }
-
- // Get the current velocity of the toolbar.
- float getVelocity() {
- return mVelocity;
- }
-
- public PointF getVisibleEndOfLayerView() {
- return new PointF(mTarget.getView().getWidth(),
- mTarget.getView().getHeight() - mMaxTranslation + mLayerViewTranslation);
- }
-
- private float bottomOfCssViewport(ImmutableViewportMetrics aMetrics) {
- return (isResizing() ? mHeightDuringResize : aMetrics.getHeight())
- + mMaxTranslation - mLayerViewTranslation;
- }
-
- private synchronized boolean getAndClearSnapRequired() {
- boolean snapRequired = mSnapRequired;
- mSnapRequired = false;
- return snapRequired;
- }
-
- void populateViewTransform(ViewTransform aTransform, ImmutableViewportMetrics aMetrics) {
- if (getAndClearSnapRequired()) {
- synchronized (mSnapRunnable) {
- ViewCompat.postOnAnimation(mTarget.getView(), mSnapRunnable);
- try {
- // hold the in-progress composite until the views have been
- // translated because otherwise there is a visible glitch.
- // don't hold for more than 100ms just in case.
- mSnapRunnable.wait(100);
- } catch (InterruptedException ie) {
- }
- }
- }
-
- aTransform.x = aMetrics.viewportRectLeft;
- aTransform.y = aMetrics.viewportRectTop;
- aTransform.width = aMetrics.viewportRectWidth;
- aTransform.height = aMetrics.viewportRectHeight;
- aTransform.scale = aMetrics.zoomFactor;
-
- aTransform.fixedLayerMarginTop = mLayerViewTranslation - mToolbarTranslation;
- float bottomOfScreen = mTarget.getView().getHeight();
- // We want to move a fixed item from "bottomOfCssViewport" to
- // "bottomOfScreen". But also the bottom margin > 0 means that bottom
- // fixed-pos items will move upwards.
- aTransform.fixedLayerMarginBottom = bottomOfCssViewport(aMetrics) - bottomOfScreen;
- //Log.v(LOGTAG, "ViewTransform is x=" + aTransform.x + " y=" + aTransform.y
- // + " z=" + aTransform.scale + " t=" + aTransform.fixedLayerMarginTop
- // + " b=" + aTransform.fixedLayerMarginBottom);
- }
-
- class DynamicToolbarAnimationTask extends RenderTask {
- private final float mStartTranslation;
- private final float mEndTranslation;
- private final boolean mImmediate;
- private final boolean mShiftLayerView;
- private boolean mContinueAnimation;
-
- public DynamicToolbarAnimationTask(float aTranslation, boolean aImmediate, boolean aShiftLayerView) {
- super(false);
- mContinueAnimation = true;
- mStartTranslation = mToolbarTranslation;
- mEndTranslation = aTranslation;
- mImmediate = aImmediate;
- mShiftLayerView = aShiftLayerView;
- }
-
- float getFinalToolbarTranslation() {
- return mEndTranslation;
- }
-
- @Override
- public boolean internalRun(long timeDelta, long currentFrameStartTime) {
- if (!mContinueAnimation) {
- return false;
- }
-
- // Calculate the progress (between 0 and 1)
- final float progress = mImmediate
- ? 1.0f
- : mInterpolator.getInterpolation(
- Math.min(1.0f, (System.nanoTime() - getStartTime())
- / (float)ANIMATION_DURATION));
-
- // This runs on the compositor thread, so we need to post the
- // actual work to the UI thread.
- ThreadUtils.assertNotOnUiThread();
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // Move the toolbar as per the animation
- mToolbarTranslation = FloatUtils.interpolate(mStartTranslation, mEndTranslation, progress);
- fireListeners();
-
- if (mShiftLayerView && progress >= 1.0f) {
- shiftLayerView(mEndTranslation);
- }
- }
- });
-
- mTarget.getView().requestRender();
- if (progress >= 1.0f) {
- mContinueAnimation = false;
- }
- return mContinueAnimation;
- }
- }
-
- class SnapMetrics {
- public final int viewportWidth;
- public final int viewportHeight;
- public final float scrollChangeY;
-
- SnapMetrics(ImmutableViewportMetrics aMetrics, float aScrollChange) {
- viewportWidth = aMetrics.viewportRectWidth;
- viewportHeight = aMetrics.viewportRectHeight;
- scrollChangeY = aScrollChange;
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/FloatSize.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.util.FloatUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public class FloatSize {
- public final float width, height;
-
- public FloatSize(FloatSize size) { width = size.width; height = size.height; }
- public FloatSize(IntSize size) { width = size.width; height = size.height; }
- public FloatSize(float aWidth, float aHeight) { width = aWidth; height = aHeight; }
-
- public FloatSize(JSONObject json) {
- try {
- width = (float)json.getDouble("width");
- height = (float)json.getDouble("height");
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public String toString() { return "(" + width + "," + height + ")"; }
-
- public boolean isPositive() {
- return (width > 0 && height > 0);
- }
-
- public boolean fuzzyEquals(FloatSize size) {
- return (FloatUtils.fuzzyEquals(size.width, width) &&
- FloatUtils.fuzzyEquals(size.height, height));
- }
-
- public FloatSize scale(float factor) {
- return new FloatSize(width * factor, height * factor);
- }
-
- /*
- * Returns the size that represents a linear transition between this size and `to` at time `t`,
- * which is on the scale [0, 1).
- */
- public FloatSize interpolate(FloatSize to, float t) {
- return new FloatSize(FloatUtils.interpolate(width, to.width, t),
- FloatUtils.interpolate(height, to.height, t));
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/FullScreenState.java
+++ /dev/null
@@ -1,12 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-public enum FullScreenState {
- NONE,
- ROOT_ELEMENT,
- NON_ROOT_ELEMENT
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/GLController.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.mozglue.JNIObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.util.Log;
-
-/**
- * This class is a singleton that tracks EGL and compositor things over
- * the lifetime of Fennec running.
- * We only ever create one C++ compositor over Fennec's lifetime, but
- * most of the Java-side objects (e.g. LayerView, GeckoLayerClient,
- * LayerRenderer) can all get destroyed and re-created if the GeckoApp
- * activity is destroyed. This GLController is never destroyed, so that
- * the mCompositorCreated field and other state variables are always
- * accurate.
- */
-public class GLController extends JNIObject {
- private static final String LOGTAG = "GeckoGLController";
-
- /* package */ LayerView mView;
- private boolean mServerSurfaceValid;
- private int mWidth, mHeight;
-
- /* This is written by the compositor thread (while the UI thread
- * is blocked on it) and read by the UI thread. */
- private volatile boolean mCompositorCreated;
-
- @WrapForJNI @Override // JNIObject
- protected native void disposeNative();
-
- // Gecko thread sets its Java instances; does not block UI thread.
- @WrapForJNI
- /* package */ native void attachToJava(GeckoLayerClient layerClient,
- NativePanZoomController npzc);
-
- @WrapForJNI
- /* package */ native void onSizeChanged(int windowWidth, int windowHeight,
- int screenWidth, int screenHeight);
-
- // Gecko thread creates compositor; blocks UI thread.
- @WrapForJNI
- private native void createCompositor(int width, int height);
-
- // Gecko thread pauses compositor; blocks UI thread.
- @WrapForJNI
- private native void pauseCompositor();
-
- // UI thread resumes compositor and notifies Gecko thread; does not block UI thread.
- @WrapForJNI
- private native void syncResumeResizeCompositor(int width, int height);
-
- @WrapForJNI
- private native void syncInvalidateAndScheduleComposite();
-
- public GLController() {
- }
-
- void serverSurfaceDestroyed() {
- ThreadUtils.assertOnUiThread();
-
- // We need to coordinate with Gecko when pausing composition, to ensure
- // that Gecko never executes a draw event while the compositor is paused.
- // This is sent synchronously to make sure that we don't attempt to use
- // any outstanding Surfaces after we call this (such as from a
- // serverSurfaceDestroyed notification), and to make sure that any in-flight
- // Gecko draw events have been processed. When this returns, composition is
- // definitely paused -- it'll synchronize with the Gecko event loop, which
- // in turn will synchronize with the compositor thread.
- if (mCompositorCreated) {
- pauseCompositor();
- }
-
- synchronized (this) {
- mServerSurfaceValid = false;
- }
- }
-
- void serverSurfaceChanged(int newWidth, int newHeight) {
- ThreadUtils.assertOnUiThread();
-
- synchronized (this) {
- mWidth = newWidth;
- mHeight = newHeight;
- mServerSurfaceValid = true;
- }
-
- updateCompositor();
- }
-
- void updateCompositor() {
- ThreadUtils.assertOnUiThread();
-
- if (mView == null) {
- return;
- }
-
- if (mCompositorCreated) {
- // If the compositor has already been created, just resume it instead. We don't need
- // to block here because if the surface is destroyed before the compositor grabs it,
- // we can handle that gracefully (i.e. the compositor will remain paused).
- resumeCompositor(mWidth, mHeight);
- return;
- }
-
- // Only try to create the compositor if we have a valid surface and gecko is up. When these
- // two conditions are satisfied, we can be relatively sure that the compositor creation will
- // happen without needing to block anywhere. Do it with a synchronous Gecko event so that the
- // Android doesn't have a chance to destroy our surface in between.
- if (isServerSurfaceValid() && mView.getLayerClient().isGeckoReady()) {
- createCompositor(mWidth, mHeight);
- compositorCreated();
- }
- }
-
- void compositorCreated() {
- // This is invoked on the compositor thread, while the java UI thread
- // is blocked on the gecko sync event in updateCompositor() above
- mCompositorCreated = true;
- }
-
- public boolean isServerSurfaceValid() {
- return mServerSurfaceValid;
- }
-
- @WrapForJNI(allowMultithread = true)
- private synchronized Object getSurface() {
- if (mView != null && isServerSurfaceValid()) {
- return mView.getSurface();
- } else {
- return null;
- }
- }
-
- void resumeCompositor(int width, int height) {
- // Asking Gecko to resume the compositor takes too long (see
- // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
- // resume the compositor directly. We still need to inform Gecko about
- // the compositor resuming, so that Gecko knows that it can now draw.
- // It is important to not notify Gecko until after the compositor has
- // been resumed, otherwise Gecko may send updates that get dropped.
- if (isServerSurfaceValid() && mCompositorCreated) {
- syncResumeResizeCompositor(width, height);
- mView.requestRender();
- }
- }
-
- /* package */ void invalidateAndScheduleComposite() {
- if (mCompositorCreated) {
- syncInvalidateAndScheduleComposite();
- }
- }
-
- @WrapForJNI
- private void destroy() {
- // The nsWindow has been closed. First mark our compositor as destroyed.
- mCompositorCreated = false;
-
- // Then clear out any pending calls on the UI thread by disposing on the UI thread.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- GLController.this.disposeNative();
- }
- });
- }
-
- public static class GLControllerException extends RuntimeException {
- public static final long serialVersionUID = 1L;
-
- GLControllerException(String e) {
- super(e);
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ /dev/null
@@ -1,1132 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.gfx.LayerView.DrawListener;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.util.FloatUtils;
-import org.mozilla.gecko.AppConstants;
-
-import android.content.Context;
-import android.graphics.PointF;
-import android.graphics.RectF;
-import android.os.SystemClock;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.List;
-
-class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
-{
- private static final String LOGTAG = "GeckoLayerClient";
- private static int sPaintSyncId = 1;
-
- private LayerRenderer mLayerRenderer;
- private boolean mLayerRendererInitialized;
-
- private final Context mContext;
- private IntSize mScreenSize;
- private IntSize mWindowSize;
- private DisplayPortMetrics mDisplayPort;
-
- private boolean mRecordDrawTimes;
- private final DrawTimingQueue mDrawTimingQueue;
-
- private VirtualLayer mRootLayer;
-
- /* The Gecko viewport as per the UI thread. Must be touched only on the UI thread.
- * If any events being sent to Gecko that are relative to the Gecko viewport position,
- * they must (a) be relative to this viewport, and (b) be sent on the UI thread to
- * avoid races. As long as these two conditions are satisfied, and the events being
- * sent to Gecko are processed in FIFO order, the events will properly be relative
- * to the Gecko viewport position. Note that if Gecko updates its viewport independently,
- * we get notified synchronously and also update this on the UI thread.
- */
- private ImmutableViewportMetrics mGeckoViewport;
-
- /*
- * The viewport metrics being used to draw the current frame. This is only
- * accessed by the compositor thread, and so needs no synchronisation.
- */
- private ImmutableViewportMetrics mFrameMetrics;
-
- private final List<DrawListener> mDrawListeners;
-
- /* Used as temporaries by syncViewportInfo */
- private final ViewTransform mCurrentViewTransform;
-
- /* Used as the return value of progressiveUpdateCallback */
- private final ProgressiveUpdateData mProgressiveUpdateData;
- private DisplayPortMetrics mProgressiveUpdateDisplayPort;
- private boolean mLastProgressiveUpdateWasLowPrecision;
- private boolean mProgressiveUpdateWasInDanger;
-
- private boolean mForceRedraw;
-
- /* The current viewport metrics.
- * This is volatile so that we can read and write to it from different threads.
- * We avoid synchronization to make getting the viewport metrics from
- * the compositor as cheap as possible. The viewport is immutable so
- * we don't need to worry about anyone mutating it while we're reading from it.
- * Specifically:
- * 1) reading mViewportMetrics from any thread is fine without synchronization
- * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
- * 3) whenever reading multiple fields from mViewportMetrics without synchronization (i.e. in
- * case 1 above) you should always first grab a local copy of the reference, and then use
- * that because mViewportMetrics might get reassigned in between reading the different
- * fields. */
- private volatile ImmutableViewportMetrics mViewportMetrics;
-
- private volatile boolean mGeckoIsReady;
-
- private final PanZoomController mPanZoomController;
- private final DynamicToolbarAnimator mToolbarAnimator;
- private final LayerView mView;
-
- /* This flag is true from the time that browser.js detects a first-paint is about to start,
- * to the time that we receive the first-paint composite notification from the compositor.
- * Note that there is a small race condition with this; if there are two paints that both
- * have the first-paint flag set, and the second paint happens concurrently with the
- * composite for the first paint, then this flag may be set to true prematurely. Fixing this
- * is possible but risky; see https://bugzilla.mozilla.org/show_bug.cgi?id=797615#c751
- */
- private volatile boolean mContentDocumentIsDisplayed;
-
- private SynthesizedEventState mPointerState;
-
- public GeckoLayerClient(Context context, LayerView view, EventDispatcher eventDispatcher) {
- // we can fill these in with dummy values because they are always written
- // to before being read
- mContext = context;
- mScreenSize = new IntSize(0, 0);
- mWindowSize = new IntSize(0, 0);
- mDisplayPort = new DisplayPortMetrics();
- mRecordDrawTimes = true;
- mDrawTimingQueue = new DrawTimingQueue();
- mCurrentViewTransform = new ViewTransform(0, 0, 1);
- mProgressiveUpdateData = new ProgressiveUpdateData();
- mProgressiveUpdateDisplayPort = new DisplayPortMetrics();
-
- mForceRedraw = true;
- DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
- mViewportMetrics = new ImmutableViewportMetrics(displayMetrics)
- .setViewportSize(view.getWidth(), view.getHeight());
-
- mFrameMetrics = mViewportMetrics;
-
- mDrawListeners = new ArrayList<DrawListener>();
- mToolbarAnimator = new DynamicToolbarAnimator(this);
- mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher);
- mView = view;
- mView.setListener(this);
- mContentDocumentIsDisplayed = true;
- }
-
- public void setOverscrollHandler(final Overscroll listener) {
- mPanZoomController.setOverscrollHandler(listener);
- }
-
- /** Attaches to root layer so that Gecko appears. */
- /* package */ boolean isGeckoReady() {
- return mGeckoIsReady;
- }
-
- @WrapForJNI
- private void onGeckoReady() {
- mGeckoIsReady = true;
-
- mRootLayer = new VirtualLayer(new IntSize(mView.getWidth(), mView.getHeight()));
- mLayerRenderer = mView.getRenderer();
-
- sendResizeEventIfNecessary(true, null);
-
- DisplayPortCalculator.initPrefs();
-
- // Gecko being ready is one of the two conditions (along with having an available
- // surface) that cause us to create the compositor. So here, now that we know gecko
- // is ready, call updateCompositor() to see if we can actually do the creation.
- // This needs to run on the UI thread so that the surface validity can't change on
- // us while we're in the middle of creating the compositor.
- mView.post(new Runnable() {
- @Override
- public void run() {
- mView.getGLController().updateCompositor();
- }
- });
- }
-
- public void destroy() {
- mPanZoomController.destroy();
- mToolbarAnimator.destroy();
- mDrawListeners.clear();
- }
-
- /**
- * Returns true if this client is fine with performing a redraw operation or false if it
- * would prefer that the action didn't take place.
- */
- private boolean getRedrawHint() {
- if (mForceRedraw) {
- mForceRedraw = false;
- return true;
- }
-
- if (!mPanZoomController.getRedrawHint()) {
- return false;
- }
-
- return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics,
- mPanZoomController.getVelocityVector(), mDisplayPort);
- }
-
- Layer getRoot() {
- return mGeckoIsReady ? mRootLayer : null;
- }
-
- public LayerView getView() {
- return mView;
- }
-
- public FloatSize getViewportSize() {
- return mViewportMetrics.getSize();
- }
-
- /**
- * The view calls this function to indicate that the viewport changed size. It must hold the
- * monitor while calling it.
- *
- * TODO: Refactor this to use an interface. Expose that interface only to the view and not
- * to the layer client. That way, the layer client won't be tempted to call this, which might
- * result in an infinite loop.
- */
- boolean setViewportSize(int width, int height, PointF scrollChange) {
- if (mViewportMetrics.viewportRectWidth == width &&
- mViewportMetrics.viewportRectHeight == height &&
- (scrollChange == null || (scrollChange.x == 0 && scrollChange.y == 0))) {
- return false;
- }
- mViewportMetrics = mViewportMetrics.setViewportSize(width, height);
- if (scrollChange != null) {
- mViewportMetrics = mPanZoomController.adjustScrollForSurfaceShift(mViewportMetrics, scrollChange);
- }
-
- if (mGeckoIsReady) {
- // here we send gecko a resize message. The code in browser.js is responsible for
- // picking up on that resize event, modifying the viewport as necessary, and informing
- // us of the new viewport.
- sendResizeEventIfNecessary(true, scrollChange);
-
- // the following call also sends gecko a message, which will be processed after the resize
- // message above has updated the viewport. this message ensures that if we have just put
- // focus in a text field, we scroll the content so that the text field is in view.
- GeckoAppShell.viewSizeChanged();
- }
- return true;
- }
-
- PanZoomController getPanZoomController() {
- return mPanZoomController;
- }
-
- DynamicToolbarAnimator getDynamicToolbarAnimator() {
- return mToolbarAnimator;
- }
-
- /* Informs Gecko that the screen size has changed. */
- private void sendResizeEventIfNecessary(boolean force, PointF scrollChange) {
- DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
-
- IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
- IntSize newWindowSize = new IntSize(mViewportMetrics.viewportRectWidth,
- mViewportMetrics.viewportRectHeight);
-
- boolean screenSizeChanged = !mScreenSize.equals(newScreenSize);
- boolean windowSizeChanged = !mWindowSize.equals(newWindowSize);
-
- if (!force && !screenSizeChanged && !windowSizeChanged) {
- return;
- }
-
- mScreenSize = newScreenSize;
- mWindowSize = newWindowSize;
-
- if (screenSizeChanged) {
- Log.d(LOGTAG, "Screen-size changed to " + mScreenSize);
- }
-
- if (windowSizeChanged) {
- Log.d(LOGTAG, "Window-size changed to " + mWindowSize);
- }
-
- if (mView != null) {
- mView.getGLController().onSizeChanged(mWindowSize.width, mWindowSize.height,
- mScreenSize.width, mScreenSize.height);
- }
-
- String json = "";
- try {
- if (scrollChange != null) {
- int id = ++sPaintSyncId;
- if (id == 0) {
- // never use 0 as that is the default value for "this is not
- // a special transaction"
- id = ++sPaintSyncId;
- }
- JSONObject jsonObj = new JSONObject();
- jsonObj.put("x", scrollChange.x / mViewportMetrics.zoomFactor);
- jsonObj.put("y", scrollChange.y / mViewportMetrics.zoomFactor);
- jsonObj.put("id", id);
- json = jsonObj.toString();
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Unable to convert point to JSON", e);
- }
- GeckoAppShell.notifyObservers("Window:Resize", json);
- }
-
- /** Sets the current page rect. You must hold the monitor while calling this. */
- private void setPageRect(RectF rect, RectF cssRect) {
- // Since the "rect" is always just a multiple of "cssRect" we don't need to
- // check both; this function assumes that both "rect" and "cssRect" are relative
- // the zoom factor in mViewportMetrics.
- if (mViewportMetrics.getCssPageRect().equals(cssRect))
- return;
-
- mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect);
-
- // Page size is owned by the layer client, so no need to notify it of
- // this change.
-
- post(new Runnable() {
- @Override
- public void run() {
- mPanZoomController.pageRectUpdated();
- mView.requestRender();
- }
- });
- }
-
- private void adjustViewport(DisplayPortMetrics displayPort) {
- // TODO: APZ For fennec might need margins information to deal with
- // the dynamic toolbar.
- if (AppConstants.MOZ_ANDROID_APZ)
- return;
-
- ImmutableViewportMetrics metrics = getViewportMetrics();
- ImmutableViewportMetrics clampedMetrics = metrics.clamp();
-
- if (displayPort == null) {
- displayPort = DisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector());
- }
-
- mDisplayPort = displayPort;
- mGeckoViewport = clampedMetrics;
-
- if (mRecordDrawTimes) {
- mDrawTimingQueue.add(displayPort);
- }
-
- GeckoAppShell.sendEventToGecko(GeckoEvent.createViewportEvent(clampedMetrics, displayPort));
- }
-
- /** Aborts any pan/zoom animation that is currently in progress. */
- private void abortPanZoomAnimation() {
- if (mPanZoomController != null) {
- post(new Runnable() {
- @Override
- public void run() {
- mPanZoomController.abortAnimation();
- }
- });
- }
- }
-
- /**
- * The different types of Viewport messages handled. All viewport events
- * expect a display-port to be returned, but can handle one not being
- * returned.
- */
- private enum ViewportMessageType {
- UPDATE, // The viewport has changed and should be entirely updated
- PAGE_SIZE // The viewport's page-size has changed
- }
-
- /** Viewport message handler. */
- private DisplayPortMetrics handleViewportMessage(ImmutableViewportMetrics messageMetrics, ViewportMessageType type) {
- synchronized (getLock()) {
- ImmutableViewportMetrics newMetrics;
- ImmutableViewportMetrics oldMetrics = getViewportMetrics();
-
- switch (type) {
- default:
- case UPDATE:
- // Keep the old viewport size
- newMetrics = messageMetrics.setViewportSize(oldMetrics.viewportRectWidth, oldMetrics.viewportRectHeight);
- if (mToolbarAnimator.isResizing()) {
- // If we're in the middle of a resize, we don't want to clobber
- // the scroll offset, so grab the one from the oldMetrics and
- // keep using that. We also don't want to abort animations,
- // because at that point we're guaranteed to not be animating
- // anyway, and calling abortPanZoomAnimation has a nasty
- // side-effect of clmaping and clobbering the metrics, which
- // we don't want here.
- newMetrics = newMetrics.setViewportOrigin(oldMetrics.viewportRectLeft, oldMetrics.viewportRectTop);
- break;
- }
- if (!oldMetrics.fuzzyEquals(newMetrics)) {
- abortPanZoomAnimation();
- }
- break;
- case PAGE_SIZE:
- // adjust the page dimensions to account for differences in zoom
- // between the rendered content (which is what Gecko tells us)
- // and our zoom level (which may have diverged).
- float scaleFactor = oldMetrics.zoomFactor / messageMetrics.zoomFactor;
- newMetrics = oldMetrics.setPageRect(RectUtils.scale(messageMetrics.getPageRect(), scaleFactor), messageMetrics.getCssPageRect());
- break;
- }
-
- // Update the Gecko-side viewport metrics. Make sure to do this
- // before modifying the metrics below.
- final ImmutableViewportMetrics geckoMetrics = newMetrics.clamp();
- post(new Runnable() {
- @Override
- public void run() {
- mGeckoViewport = geckoMetrics;
- }
- });
-
- setViewportMetrics(newMetrics, type == ViewportMessageType.UPDATE);
- mDisplayPort = DisplayPortCalculator.calculate(getViewportMetrics(), null);
- }
- return mDisplayPort;
- }
-
- @WrapForJNI
- DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) {
- return null;
- }
-
- @WrapForJNI
- void contentDocumentChanged() {
- mContentDocumentIsDisplayed = false;
- }
-
- @WrapForJNI
- boolean isContentDocumentDisplayed() {
- return mContentDocumentIsDisplayed;
- }
-
- // This is called on the Gecko thread to determine if we're still interested
- // in the update of this display-port to continue. We can return true here
- // to abort the current update and continue with any subsequent ones. This
- // is useful for slow-to-render pages when the display-port starts lagging
- // behind enough that continuing to draw it is wasted effort.
- @WrapForJNI(allowMultithread = true)
- public ProgressiveUpdateData progressiveUpdateCallback(boolean aHasPendingNewThebesContent,
- float x, float y, float width, float height,
- float resolution, boolean lowPrecision) {
- // Reset the checkerboard risk flag when switching to low precision
- // rendering.
- if (lowPrecision && !mLastProgressiveUpdateWasLowPrecision) {
- // Skip low precision rendering until we're at risk of checkerboarding.
- if (!mProgressiveUpdateWasInDanger) {
- mProgressiveUpdateData.abort = true;
- return mProgressiveUpdateData;
- }
- mProgressiveUpdateWasInDanger = false;
- }
- mLastProgressiveUpdateWasLowPrecision = lowPrecision;
-
- // Grab a local copy of the last display-port sent to Gecko and the
- // current viewport metrics to avoid races when accessing them.
- DisplayPortMetrics displayPort = mDisplayPort;
- ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
- mProgressiveUpdateData.setViewport(viewportMetrics);
- mProgressiveUpdateData.abort = false;
-
- // Always abort updates if the resolution has changed. There's no use
- // in drawing at the incorrect resolution.
- if (!FloatUtils.fuzzyEquals(resolution, viewportMetrics.zoomFactor)) {
- Log.d(LOGTAG, "Aborting draw due to resolution change: " + resolution + " != " + viewportMetrics.zoomFactor);
- mProgressiveUpdateData.abort = true;
- return mProgressiveUpdateData;
- }
-
- // Store the high precision displayport for comparison when doing low
- // precision updates.
- if (!lowPrecision) {
- if (!FloatUtils.fuzzyEquals(resolution, mProgressiveUpdateDisplayPort.resolution) ||
- !FloatUtils.fuzzyEquals(x, mProgressiveUpdateDisplayPort.getLeft()) ||
- !FloatUtils.fuzzyEquals(y, mProgressiveUpdateDisplayPort.getTop()) ||
- !FloatUtils.fuzzyEquals(x + width, mProgressiveUpdateDisplayPort.getRight()) ||
- !FloatUtils.fuzzyEquals(y + height, mProgressiveUpdateDisplayPort.getBottom())) {
- mProgressiveUpdateDisplayPort =
- new DisplayPortMetrics(x, y, x + width, y + height, resolution);
- }
- }
-
- // If we're not doing low precision draws and we're about to
- // checkerboard, enable low precision drawing.
- if (!lowPrecision && !mProgressiveUpdateWasInDanger) {
- if (DisplayPortCalculator.aboutToCheckerboard(viewportMetrics,
- mPanZoomController.getVelocityVector(), mProgressiveUpdateDisplayPort)) {
- mProgressiveUpdateWasInDanger = true;
- }
- }
-
- // XXX All sorts of rounding happens inside Gecko that becomes hard to
- // account exactly for. Given we align the display-port to tile
- // boundaries (and so they rarely vary by sub-pixel amounts), just
- // check that values are within a couple of pixels of the
- // display-port bounds.
-
- // Never abort drawing if we can't be sure we've sent a more recent
- // display-port. If we abort updating when we shouldn't, we can end up
- // with blank regions on the screen and we open up the risk of entering
- // an endless updating cycle.
- if (Math.abs(displayPort.getLeft() - mProgressiveUpdateDisplayPort.getLeft()) <= 2 &&
- Math.abs(displayPort.getTop() - mProgressiveUpdateDisplayPort.getTop()) <= 2 &&
- Math.abs(displayPort.getBottom() - mProgressiveUpdateDisplayPort.getBottom()) <= 2 &&
- Math.abs(displayPort.getRight() - mProgressiveUpdateDisplayPort.getRight()) <= 2) {
- return mProgressiveUpdateData;
- }
-
- // Abort updates when the display-port no longer contains the visible
- // area of the page (that is, the viewport cropped by the page
- // boundaries).
- // XXX This makes the assumption that we never let the visible area of
- // the page fall outside of the display-port.
- if (Math.max(viewportMetrics.viewportRectLeft, viewportMetrics.pageRectLeft) + 1 < x ||
- Math.max(viewportMetrics.viewportRectTop, viewportMetrics.pageRectTop) + 1 < y ||
- Math.min(viewportMetrics.viewportRectRight(), viewportMetrics.pageRectRight) - 1 > x + width ||
- Math.min(viewportMetrics.viewportRectBottom(), viewportMetrics.pageRectBottom) - 1 > y + height) {
- Log.d(LOGTAG, "Aborting update due to viewport not in display-port");
- mProgressiveUpdateData.abort = true;
-
- // Enable low-precision drawing, as we're likely to be in danger if
- // this situation has been encountered.
- mProgressiveUpdateWasInDanger = true;
-
- return mProgressiveUpdateData;
- }
-
- // Abort drawing stale low-precision content if there's a more recent
- // display-port in the pipeline.
- if (lowPrecision && !aHasPendingNewThebesContent) {
- mProgressiveUpdateData.abort = true;
- }
- return mProgressiveUpdateData;
- }
-
- /** The compositor invokes this function just before compositing a frame where the document
- * is different from the document composited on the last frame. In these cases, the viewport
- * information we have in Java is no longer valid and needs to be replaced with the new
- * viewport information provided. setPageRect will never be invoked on the same frame that
- * this function is invoked on; and this function will always be called prior to syncViewportInfo.
- */
- @WrapForJNI(allowMultithread = true)
- public void setFirstPaintViewport(float offsetX, float offsetY, float zoom,
- float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
- synchronized (getLock()) {
- ImmutableViewportMetrics currentMetrics = getViewportMetrics();
-
- RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
- RectF pageRect = RectUtils.scaleAndRound(cssPageRect, zoom);
-
- final ImmutableViewportMetrics newMetrics = currentMetrics
- .setViewportOrigin(offsetX, offsetY)
- .setZoomFactor(zoom)
- .setPageRect(pageRect, cssPageRect);
- // Since we have switched to displaying a different document, we need to update any
- // viewport-related state we have lying around. This includes mGeckoViewport and
- // mViewportMetrics. Usually this information is updated via handleViewportMessage
- // while we remain on the same document.
- post(new Runnable() {
- @Override
- public void run() {
- mGeckoViewport = newMetrics;
- }
- });
-
- setViewportMetrics(newMetrics);
-
- // At this point, we have just switched to displaying a different document than we
- // we previously displaying. This means we need to abort any panning/zooming animations
- // that are in progress and send an updated display port request to browser.js as soon
- // as possible. The call to PanZoomController.abortAnimation accomplishes this by calling the
- // forceRedraw function, which sends the viewport to gecko. The display port request is
- // actually a full viewport update, which is fine because if browser.js has somehow moved to
- // be out of sync with this first-paint viewport, then we force them back in sync.
- abortPanZoomAnimation();
-
- // Indicate that the document is about to be composited so the
- // LayerView background can be removed.
- if (mView.getPaintState() == LayerView.PAINT_START) {
- mView.setPaintState(LayerView.PAINT_BEFORE_FIRST);
- }
- }
- DisplayPortCalculator.resetPageState();
- mDrawTimingQueue.reset();
-
- mContentDocumentIsDisplayed = true;
- }
-
- /** The compositor invokes this function whenever it determines that the page rect
- * has changed (based on the information it gets from layout). If setFirstPaintViewport
- * is invoked on a frame, then this function will not be. For any given frame, this
- * function will be invoked before syncViewportInfo.
- */
- @WrapForJNI(allowMultithread = true)
- public void setPageRect(float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
- synchronized (getLock()) {
- RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
- float ourZoom = getViewportMetrics().zoomFactor;
- setPageRect(RectUtils.scale(cssPageRect, ourZoom), cssPageRect);
- // Here the page size of the document has changed, but the document being displayed
- // is still the same. Therefore, we don't need to send anything to browser.js; any
- // changes we need to make to the display port will get sent the next time we call
- // adjustViewport().
- }
- }
-
- /** The compositor invokes this function on every frame to figure out what part of the
- * page to display, and to inform Java of the current display port. Since it is called
- * on every frame, it needs to be ultra-fast.
- * It avoids taking any locks or allocating any objects. We keep around a
- * mCurrentViewTransform so we don't need to allocate a new ViewTransform
- * every time we're called. NOTE: we might be able to return a ImmutableViewportMetrics
- * which would avoid the copy into mCurrentViewTransform.
- */
- @WrapForJNI(allowMultithread = true)
- public ViewTransform syncViewportInfo(int x, int y, int width, int height, float resolution, boolean layersUpdated,
- int paintSyncId) {
- // getViewportMetrics is thread safe so we don't need to synchronize.
- // We save the viewport metrics here, so we later use it later in
- // createFrame (which will be called by nsWindow::DrawWindowUnderlay on
- // the native side, by the compositor). The viewport
- // metrics can change between here and there, as it's accessed outside
- // of the compositor thread.
- mFrameMetrics = getViewportMetrics();
-
- if (paintSyncId == sPaintSyncId) {
- mToolbarAnimator.scrollChangeResizeCompleted();
- }
- mToolbarAnimator.populateViewTransform(mCurrentViewTransform, mFrameMetrics);
-
- if (mRootLayer != null) {
- mRootLayer.setPositionAndResolution(
- x, y, x + width, y + height,
- resolution);
- }
-
- if (layersUpdated && mRecordDrawTimes) {
- // If we got a layers update, that means a draw finished. Check to see if the area drawn matches
- // one of our requested displayports; if it does calculate the draw time and notify the
- // DisplayPortCalculator
- DisplayPortMetrics drawn = new DisplayPortMetrics(x, y, x + width, y + height, resolution);
- long time = mDrawTimingQueue.findTimeFor(drawn);
- if (time >= 0) {
- long now = SystemClock.uptimeMillis();
- time = now - time;
- mRecordDrawTimes = DisplayPortCalculator.drawTimeUpdate(time, width * height);
- }
- }
-
- if (layersUpdated) {
- for (DrawListener listener : mDrawListeners) {
- listener.drawFinished();
- }
- }
-
- return mCurrentViewTransform;
- }
-
- @WrapForJNI(allowMultithread = true)
- public ViewTransform syncFrameMetrics(float scrollX, float scrollY, float zoom,
- float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom,
- int dpX, int dpY, int dpWidth, int dpHeight, float paintedResolution,
- boolean layersUpdated, int paintSyncId)
- {
- // TODO: optimize this so it doesn't create so much garbage - it's a
- // hot path
- RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
- synchronized (getLock()) {
- mViewportMetrics = mViewportMetrics.setViewportOrigin(scrollX, scrollY)
- .setZoomFactor(zoom)
- .setPageRect(RectUtils.scale(cssPageRect, zoom), cssPageRect);
- }
- return syncViewportInfo(dpX, dpY, dpWidth, dpHeight, paintedResolution,
- layersUpdated, paintSyncId);
- }
-
- class PointerInfo {
- // We reserve one pointer ID for the mouse, so that tests don't have
- // to worry about tracking pointer IDs if they just want to test mouse
- // event synthesization. If somebody tries to use this ID for a
- // synthesized touch event we'll throw an exception.
- public static final int RESERVED_MOUSE_POINTER_ID = 100000;
-
- public int pointerId;
- public int source;
- public int screenX;
- public int screenY;
- public double pressure;
- public int orientation;
-
- public MotionEvent.PointerCoords getCoords() {
- MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
- coords.orientation = orientation;
- coords.pressure = (float)pressure;
- coords.x = screenX;
- coords.y = screenY;
- return coords;
- }
- }
-
- class SynthesizedEventState {
- public final ArrayList<PointerInfo> pointers;
- public long downTime;
-
- SynthesizedEventState() {
- pointers = new ArrayList<PointerInfo>();
- }
-
- int getPointerIndex(int pointerId) {
- for (int i = 0; i < pointers.size(); i++) {
- if (pointers.get(i).pointerId == pointerId) {
- return i;
- }
- }
- return -1;
- }
-
- int addPointer(int pointerId, int source) {
- PointerInfo info = new PointerInfo();
- info.pointerId = pointerId;
- info.source = source;
- pointers.add(info);
- return pointers.size() - 1;
- }
-
- int getPointerCount(int source) {
- int count = 0;
- for (int i = 0; i < pointers.size(); i++) {
- if (pointers.get(i).source == source) {
- count++;
- }
- }
- return count;
- }
-
- MotionEvent.PointerProperties[] getPointerProperties(int source) {
- MotionEvent.PointerProperties[] props = new MotionEvent.PointerProperties[getPointerCount(source)];
- int index = 0;
- for (int i = 0; i < pointers.size(); i++) {
- if (pointers.get(i).source == source) {
- MotionEvent.PointerProperties p = new MotionEvent.PointerProperties();
- p.id = pointers.get(i).pointerId;
- switch (source) {
- case InputDevice.SOURCE_TOUCHSCREEN:
- p.toolType = MotionEvent.TOOL_TYPE_FINGER;
- break;
- case InputDevice.SOURCE_MOUSE:
- p.toolType = MotionEvent.TOOL_TYPE_MOUSE;
- break;
- }
- props[index++] = p;
- }
- }
- return props;
- }
-
- MotionEvent.PointerCoords[] getPointerCoords(int source) {
- MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[getPointerCount(source)];
- int index = 0;
- for (int i = 0; i < pointers.size(); i++) {
- if (pointers.get(i).source == source) {
- coords[index++] = pointers.get(i).getCoords();
- }
- }
- return coords;
- }
- }
-
- private void synthesizeNativePointer(int source, int pointerId,
- int eventType, int screenX, int screenY, double pressure,
- int orientation)
- {
- Log.d(LOGTAG, "Synthesizing pointer from " + source + " id " + pointerId + " at " + screenX + ", " + screenY);
-
- if (mPointerState == null) {
- mPointerState = new SynthesizedEventState();
- }
-
- // Find the pointer if it already exists
- int pointerIndex = mPointerState.getPointerIndex(pointerId);
-
- // Event-specific handling
- switch (eventType) {
- case MotionEvent.ACTION_POINTER_UP:
- if (pointerIndex < 0) {
- Log.d(LOGTAG, "Requested synthesis of a pointer-up for a pointer that doesn't exist!");
- return;
- }
- if (mPointerState.pointers.size() == 1) {
- // Last pointer is going up
- eventType = MotionEvent.ACTION_UP;
- }
- break;
- case MotionEvent.ACTION_CANCEL:
- if (pointerIndex < 0) {
- Log.d(LOGTAG, "Requested synthesis of a pointer-cancel for a pointer that doesn't exist!");
- return;
- }
- break;
- case MotionEvent.ACTION_POINTER_DOWN:
- if (pointerIndex < 0) {
- // Adding a new pointer
- pointerIndex = mPointerState.addPointer(pointerId, source);
- if (pointerIndex == 0) {
- // first pointer
- eventType = MotionEvent.ACTION_DOWN;
- mPointerState.downTime = SystemClock.uptimeMillis();
- }
- } else {
- // We're moving an existing pointer
- eventType = MotionEvent.ACTION_MOVE;
- }
- break;
- case MotionEvent.ACTION_HOVER_MOVE:
- if (pointerIndex < 0) {
- // Mouse-move a pointer without it going "down". However
- // in order to send the right MotionEvent without a lot of
- // duplicated code, we add the pointer to mPointerState,
- // and then remove it at the bottom of this function.
- pointerIndex = mPointerState.addPointer(pointerId, source);
- } else {
- // We're moving an existing mouse pointer that went down.
- eventType = MotionEvent.ACTION_MOVE;
- }
- break;
- }
-
- // Update the pointer with the new info
- PointerInfo info = mPointerState.pointers.get(pointerIndex);
- info.screenX = screenX;
- info.screenY = screenY;
- info.pressure = pressure;
- info.orientation = orientation;
-
- // Dispatch the event
- int action = (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
- action &= MotionEvent.ACTION_POINTER_INDEX_MASK;
- action |= (eventType & MotionEvent.ACTION_MASK);
- boolean isButtonDown = (source == InputDevice.SOURCE_MOUSE) &&
- (eventType == MotionEvent.ACTION_DOWN || eventType == MotionEvent.ACTION_MOVE);
- final MotionEvent event = MotionEvent.obtain(
- /*downTime*/ mPointerState.downTime,
- /*eventTime*/ SystemClock.uptimeMillis(),
- /*action*/ action,
- /*pointerCount*/ mPointerState.getPointerCount(source),
- /*pointerProperties*/ mPointerState.getPointerProperties(source),
- /*pointerCoords*/ mPointerState.getPointerCoords(source),
- /*metaState*/ 0,
- /*buttonState*/ (isButtonDown ? MotionEvent.BUTTON_PRIMARY : 0),
- /*xPrecision*/ 0,
- /*yPrecision*/ 0,
- /*deviceId*/ 0,
- /*edgeFlags*/ 0,
- /*source*/ source,
- /*flags*/ 0);
- mView.post(new Runnable() {
- @Override
- public void run() {
- event.offsetLocation(0, mView.getSurfaceTranslation());
- mView.dispatchTouchEvent(event);
- }
- });
-
- // Forget about removed pointers
- if (eventType == MotionEvent.ACTION_POINTER_UP ||
- eventType == MotionEvent.ACTION_UP ||
- eventType == MotionEvent.ACTION_CANCEL ||
- eventType == MotionEvent.ACTION_HOVER_MOVE)
- {
- mPointerState.pointers.remove(pointerIndex);
- }
- }
-
- @WrapForJNI
- public void synthesizeNativeTouchPoint(int pointerId, int eventType, int screenX,
- int screenY, double pressure, int orientation)
- {
- if (pointerId == PointerInfo.RESERVED_MOUSE_POINTER_ID) {
- throw new IllegalArgumentException("Use a different pointer ID in your test, this one is reserved for mouse");
- }
- synthesizeNativePointer(InputDevice.SOURCE_TOUCHSCREEN, pointerId,
- eventType, screenX, screenY, pressure, orientation);
- }
-
- @WrapForJNI
- public void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
- synthesizeNativePointer(InputDevice.SOURCE_MOUSE, PointerInfo.RESERVED_MOUSE_POINTER_ID,
- eventType, screenX, screenY, 0, 0);
- }
-
- @WrapForJNI(allowMultithread = true)
- public LayerRenderer.Frame createFrame() {
- // Create the shaders and textures if necessary.
- if (!mLayerRendererInitialized) {
- if (mLayerRenderer == null) {
- return null;
- }
- mLayerRenderer.checkMonitoringEnabled();
- mLayerRenderer.createDefaultProgram();
- mLayerRendererInitialized = true;
- }
-
- try {
- return mLayerRenderer.createFrame(mFrameMetrics);
- } catch (Exception e) {
- Log.w(LOGTAG, e);
- return null;
- }
- }
-
- @WrapForJNI(allowMultithread = true)
- public void activateProgram() {
- mLayerRenderer.activateDefaultProgram();
- }
-
- @WrapForJNI(allowMultithread = true)
- public void deactivateProgramAndRestoreState(boolean enableScissor,
- int scissorX, int scissorY, int scissorW, int scissorH)
- {
- mLayerRenderer.deactivateDefaultProgram();
- mLayerRenderer.restoreState(enableScissor, scissorX, scissorY, scissorW, scissorH);
- }
-
- private void geometryChanged(DisplayPortMetrics displayPort) {
- /* Let Gecko know if the screensize has changed */
- sendResizeEventIfNecessary(false, null);
- if (getRedrawHint()) {
- adjustViewport(displayPort);
- }
- }
-
- /** Implementation of LayerView.Listener */
- @Override
- public void renderRequested() {
- final GLController glController = mView.getGLController();
- if (glController != null) {
- glController.invalidateAndScheduleComposite();
- }
- }
-
- /** Implementation of LayerView.Listener */
- @Override
- public void sizeChanged(int width, int height) {
- // We need to make sure a draw happens synchronously at this point,
- // but resizing the surface before the SurfaceView has resized will
- // cause a visible jump.
- mView.getGLController().resumeCompositor(width, height);
- }
-
- /** Implementation of LayerView.Listener */
- @Override
- public void surfaceChanged(int width, int height) {
- IntSize viewportSize = mToolbarAnimator.getViewportSize();
- setViewportSize(viewportSize.width, viewportSize.height, null);
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public ImmutableViewportMetrics getViewportMetrics() {
- return mViewportMetrics;
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public FullScreenState getFullScreenState() {
- return mView.getFullScreenState();
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public PointF getVisibleEndOfLayerView() {
- return mToolbarAnimator.getVisibleEndOfLayerView();
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public void setAnimationTarget(ImmutableViewportMetrics metrics) {
- if (mGeckoIsReady) {
- // We know what the final viewport of the animation is going to be, so
- // immediately request a draw of that area by setting the display port
- // accordingly. This way we should have the content pre-rendered by the
- // time the animation is done.
- DisplayPortMetrics displayPort = DisplayPortCalculator.calculate(metrics, null);
- adjustViewport(displayPort);
- }
- }
-
- /** Implementation of PanZoomTarget
- * You must hold the monitor while calling this.
- */
- @Override
- public void setViewportMetrics(ImmutableViewportMetrics metrics) {
- setViewportMetrics(metrics, true);
- }
-
- /*
- * You must hold the monitor while calling this.
- */
- private void setViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko) {
- // This class owns the viewport size and the fixed layer margins; don't let other pieces
- // of code clobber either of them. The only place the viewport size should ever be
- // updated is in GeckoLayerClient.setViewportSize, and the only place the margins should
- // ever be updated is in GeckoLayerClient.setFixedLayerMargins; both of these assign to
- // mViewportMetrics directly.
- metrics = metrics.setViewportSize(mViewportMetrics.viewportRectWidth, mViewportMetrics.viewportRectHeight);
- mViewportMetrics = metrics;
-
- viewportMetricsChanged(notifyGecko);
- }
-
- /*
- * You must hold the monitor while calling this.
- */
- private void viewportMetricsChanged(boolean notifyGecko) {
- mToolbarAnimator.onMetricsChanged(mViewportMetrics);
-
- mView.requestRender();
- if (notifyGecko && mGeckoIsReady) {
- geometryChanged(null);
- }
- }
-
- /*
- * Updates the viewport metrics, overriding the viewport size and margins
- * which are normally retained when calling setViewportMetrics.
- * You must hold the monitor while calling this.
- */
- void forceViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko, boolean forceRedraw) {
- if (forceRedraw) {
- mForceRedraw = true;
- }
- mViewportMetrics = metrics;
- viewportMetricsChanged(notifyGecko);
- }
-
- /** Implementation of PanZoomTarget
- * Scroll the viewport by a certain amount. This will take viewport margins
- * and margin animation into account. If margins are currently animating,
- * this will just go ahead and modify the viewport origin, otherwise the
- * delta will be applied to the margins and the remainder will be applied to
- * the viewport origin.
- *
- * You must hold the monitor while calling this.
- */
- @Override
- public void scrollBy(float dx, float dy) {
- // Set mViewportMetrics manually so the margin changes take.
- mViewportMetrics = mViewportMetrics.offsetViewportBy(dx, dy);
- viewportMetricsChanged(true);
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public void panZoomStopped() {
- mToolbarAnimator.onPanZoomStopped();
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public void forceRedraw(DisplayPortMetrics displayPort) {
- mForceRedraw = true;
- if (mGeckoIsReady) {
- geometryChanged(displayPort);
- }
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public boolean post(Runnable action) {
- return mView.post(action);
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public void postRenderTask(RenderTask task) {
- mView.postRenderTask(task);
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public void removeRenderTask(RenderTask task) {
- mView.removeRenderTask(task);
- }
-
- /** Implementation of PanZoomTarget */
- @Override
- public Object getLock() {
- return this;
- }
-
- /** Implementation of PanZoomTarget
- * Converts a point from layer view coordinates to layer coordinates. In other words, given a
- * point measured in pixels from the top left corner of the layer view, returns the point in
- * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the
- * events being sent to Gecko are processed in FIFO order, this calculation should always be
- * correct.
- */
- @Override
- public PointF convertViewPointToLayerPoint(PointF viewPoint) {
- if (!mGeckoIsReady) {
- return null;
- }
-
- ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
- PointF origin = viewportMetrics.getOrigin();
- float zoom = viewportMetrics.zoomFactor;
- ImmutableViewportMetrics geckoViewport = (AppConstants.MOZ_ANDROID_APZ ? mViewportMetrics : mGeckoViewport);
- PointF geckoOrigin = geckoViewport.getOrigin();
- float geckoZoom = geckoViewport.zoomFactor;
-
- // viewPoint + origin - offset gives the coordinate in device pixels from the top-left corner of the page.
- // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page.
- // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from
- // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from
- // the current Gecko coordinate in CSS pixels.
- PointF layerPoint = new PointF(
- ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
- ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
-
- return layerPoint;
- }
-
- @Override
- public void setScrollingRootContent(boolean isRootContent) {
- mToolbarAnimator.setScrollingRootContent(isRootContent);
- }
-
- public void addDrawListener(DrawListener listener) {
- mDrawListeners.add(listener);
- }
-
- public void removeDrawListener(DrawListener listener) {
- mDrawListeners.remove(listener);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
+++ /dev/null
@@ -1,316 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.util.FloatUtils;
-
-import android.graphics.PointF;
-import android.graphics.RectF;
-import android.util.DisplayMetrics;
-
-/**
- * ImmutableViewportMetrics are used to store the viewport metrics
- * in way that we can access a version of them from multiple threads
- * without having to take a lock
- */
-public class ImmutableViewportMetrics {
-
- // We need to flatten the RectF and FloatSize structures
- // because Java doesn't have the concept of const classes
- public final float pageRectLeft;
- public final float pageRectTop;
- public final float pageRectRight;
- public final float pageRectBottom;
- public final float cssPageRectLeft;
- public final float cssPageRectTop;
- public final float cssPageRectRight;
- public final float cssPageRectBottom;
- public final float viewportRectLeft;
- public final float viewportRectTop;
- public final int viewportRectWidth;
- public final int viewportRectHeight;
-
- public final float zoomFactor;
- public final boolean isRTL;
-
- public ImmutableViewportMetrics(DisplayMetrics metrics) {
- viewportRectLeft = pageRectLeft = cssPageRectLeft = 0;
- viewportRectTop = pageRectTop = cssPageRectTop = 0;
- viewportRectWidth = metrics.widthPixels;
- viewportRectHeight = metrics.heightPixels;
- pageRectRight = cssPageRectRight = metrics.widthPixels;
- pageRectBottom = cssPageRectBottom = metrics.heightPixels;
- zoomFactor = 1.0f;
- isRTL = false;
- }
-
- /** This constructor is used by native code in AndroidJavaWrappers.cpp, be
- * careful when modifying the signature.
- */
- @WrapForJNI(allowMultithread = true)
- public ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
- float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
- float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
- float aViewportRectLeft, float aViewportRectTop, int aViewportRectWidth,
- int aViewportRectHeight, float aZoomFactor)
- {
- this(aPageRectLeft, aPageRectTop,
- aPageRectRight, aPageRectBottom, aCssPageRectLeft,
- aCssPageRectTop, aCssPageRectRight, aCssPageRectBottom,
- aViewportRectLeft, aViewportRectTop, aViewportRectWidth,
- aViewportRectHeight, aZoomFactor, false);
- }
-
- private ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
- float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
- float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
- float aViewportRectLeft, float aViewportRectTop, int aViewportRectWidth,
- int aViewportRectHeight, float aZoomFactor, boolean aIsRTL)
- {
- pageRectLeft = aPageRectLeft;
- pageRectTop = aPageRectTop;
- pageRectRight = aPageRectRight;
- pageRectBottom = aPageRectBottom;
- cssPageRectLeft = aCssPageRectLeft;
- cssPageRectTop = aCssPageRectTop;
- cssPageRectRight = aCssPageRectRight;
- cssPageRectBottom = aCssPageRectBottom;
- viewportRectLeft = aViewportRectLeft;
- viewportRectTop = aViewportRectTop;
- viewportRectWidth = aViewportRectWidth;
- viewportRectHeight = aViewportRectHeight;
- zoomFactor = aZoomFactor;
- isRTL = aIsRTL;
- }
-
- public float getWidth() {
- return viewportRectWidth;
- }
-
- public float getHeight() {
- return viewportRectHeight;
- }
-
- public float viewportRectRight() {
- return viewportRectLeft + viewportRectWidth;
- }
-
- public float viewportRectBottom() {
- return viewportRectTop + viewportRectHeight;
- }
-
- public PointF getOrigin() {
- return new PointF(viewportRectLeft, viewportRectTop);
- }
-
- public FloatSize getSize() {
- return new FloatSize(viewportRectWidth, viewportRectHeight);
- }
-
- public RectF getViewport() {
- return new RectF(viewportRectLeft,
- viewportRectTop,
- viewportRectRight(),
- viewportRectBottom());
- }
-
- public RectF getCssViewport() {
- return RectUtils.scale(getViewport(), 1 / zoomFactor);
- }
-
- public RectF getPageRect() {
- return new RectF(pageRectLeft, pageRectTop, pageRectRight, pageRectBottom);
- }
-
- public float getPageWidth() {
- return pageRectRight - pageRectLeft;
- }
-
- public float getPageHeight() {
- return pageRectBottom - pageRectTop;
- }
-
- public RectF getCssPageRect() {
- return new RectF(cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom);
- }
-
- public RectF getOverscroll() {
- return new RectF(Math.max(0, pageRectLeft - viewportRectLeft),
- Math.max(0, pageRectTop - viewportRectTop),
- Math.max(0, viewportRectRight() - pageRectRight),
- Math.max(0, viewportRectBottom() - pageRectBottom));
- }
-
- /*
- * Returns the viewport metrics that represent a linear transition between "this" and "to" at
- * time "t", which is on the scale [0, 1). This function interpolates all values stored in
- * the viewport metrics.
- */
- public ImmutableViewportMetrics interpolate(ImmutableViewportMetrics to, float t) {
- return new ImmutableViewportMetrics(
- FloatUtils.interpolate(pageRectLeft, to.pageRectLeft, t),
- FloatUtils.interpolate(pageRectTop, to.pageRectTop, t),
- FloatUtils.interpolate(pageRectRight, to.pageRectRight, t),
- FloatUtils.interpolate(pageRectBottom, to.pageRectBottom, t),
- FloatUtils.interpolate(cssPageRectLeft, to.cssPageRectLeft, t),
- FloatUtils.interpolate(cssPageRectTop, to.cssPageRectTop, t),
- FloatUtils.interpolate(cssPageRectRight, to.cssPageRectRight, t),
- FloatUtils.interpolate(cssPageRectBottom, to.cssPageRectBottom, t),
- FloatUtils.interpolate(viewportRectLeft, to.viewportRectLeft, t),
- FloatUtils.interpolate(viewportRectTop, to.viewportRectTop, t),
- (int)FloatUtils.interpolate(viewportRectWidth, to.viewportRectWidth, t),
- (int)FloatUtils.interpolate(viewportRectHeight, to.viewportRectHeight, t),
- FloatUtils.interpolate(zoomFactor, to.zoomFactor, t),
- t >= 0.5 ? to.isRTL : isRTL);
- }
-
- public ImmutableViewportMetrics setViewportSize(int width, int height) {
- if (width == viewportRectWidth && height == viewportRectHeight) {
- return this;
- }
-
- return new ImmutableViewportMetrics(
- pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
- cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
- viewportRectLeft, viewportRectTop, width, height,
- zoomFactor, isRTL);
- }
-
- public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
- return new ImmutableViewportMetrics(
- pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
- cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
- newOriginX, newOriginY, viewportRectWidth, viewportRectHeight,
- zoomFactor, isRTL);
- }
-
- public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
- return new ImmutableViewportMetrics(
- pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
- cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
- viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
- newZoomFactor, isRTL);
- }
-
- public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
- return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
- }
-
- public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
- if (isRTL) {
- return setViewportOrigin(
- Math.min(pageRectRight - getWidth(), Math.max(viewportRectLeft + dx, pageRectLeft)),
- Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
- }
- return setViewportOrigin(
- Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidth())),
- Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
- }
-
- public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) {
- return new ImmutableViewportMetrics(
- pageRect.left, pageRect.top, pageRect.right, pageRect.bottom,
- cssPageRect.left, cssPageRect.top, cssPageRect.right, cssPageRect.bottom,
- viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
- zoomFactor, isRTL);
- }
-
- public ImmutableViewportMetrics setPageRectFrom(ImmutableViewportMetrics aMetrics) {
- if (aMetrics.cssPageRectLeft == cssPageRectLeft &&
- aMetrics.cssPageRectTop == cssPageRectTop &&
- aMetrics.cssPageRectRight == cssPageRectRight &&
- aMetrics.cssPageRectBottom == cssPageRectBottom) {
- return this;
- }
- RectF css = aMetrics.getCssPageRect();
- return setPageRect(RectUtils.scale(css, zoomFactor), css);
- }
-
- public ImmutableViewportMetrics setIsRTL(boolean aIsRTL) {
- if (isRTL == aIsRTL) {
- return this;
- }
-
- return new ImmutableViewportMetrics(
- pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
- cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
- viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
- zoomFactor, aIsRTL);
- }
-
- /* This will set the zoom factor and re-scale page-size and viewport offset
- * accordingly. The given focus will remain at the same point on the screen
- * after scaling.
- */
- public ImmutableViewportMetrics scaleTo(float newZoomFactor, PointF focus) {
- // cssPageRect* is invariant, since we're setting the scale factor
- // here. The page rect is based on the CSS page rect.
- float newPageRectLeft = cssPageRectLeft * newZoomFactor;
- float newPageRectTop = cssPageRectTop * newZoomFactor;
- float newPageRectRight = cssPageRectLeft + ((cssPageRectRight - cssPageRectLeft) * newZoomFactor);
- float newPageRectBottom = cssPageRectTop + ((cssPageRectBottom - cssPageRectTop) * newZoomFactor);
-
- PointF origin = getOrigin();
- origin.offset(focus.x, focus.y);
- origin = PointUtils.scale(origin, newZoomFactor / zoomFactor);
- origin.offset(-focus.x, -focus.y);
-
- return new ImmutableViewportMetrics(
- newPageRectLeft, newPageRectTop, newPageRectRight, newPageRectBottom,
- cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
- origin.x, origin.y, viewportRectWidth, viewportRectHeight,
- newZoomFactor, isRTL);
- }
-
- /** Clamps the viewport to remain within the page rect. */
- public ImmutableViewportMetrics clamp() {
- RectF newViewport = getViewport();
-
- // The viewport bounds ought to never exceed the page bounds.
- if (newViewport.right > pageRectRight)
- newViewport.offset((pageRectRight) - newViewport.right, 0);
- if (newViewport.left < pageRectLeft)
- newViewport.offset(pageRectLeft - newViewport.left, 0);
-
- if (newViewport.bottom > pageRectBottom)
- newViewport.offset(0, (pageRectBottom) - newViewport.bottom);
- if (newViewport.top < pageRectTop)
- newViewport.offset(0, pageRectTop - newViewport.top);
-
- // Note that since newViewport is only translated around, the viewport's
- // width and height are unchanged.
- return new ImmutableViewportMetrics(
- pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
- cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
- newViewport.left, newViewport.top, viewportRectWidth, viewportRectHeight,
- zoomFactor, isRTL);
- }
-
- public boolean fuzzyEquals(ImmutableViewportMetrics other) {
- // Don't bother checking the pageRectXXX values because they are a product
- // of the cssPageRectXXX values and the zoomFactor, except with more rounding
- // error. Checking those is both inefficient and can lead to false negatives.
- return FloatUtils.fuzzyEquals(cssPageRectLeft, other.cssPageRectLeft)
- && FloatUtils.fuzzyEquals(cssPageRectTop, other.cssPageRectTop)
- && FloatUtils.fuzzyEquals(cssPageRectRight, other.cssPageRectRight)
- && FloatUtils.fuzzyEquals(cssPageRectBottom, other.cssPageRectBottom)
- && FloatUtils.fuzzyEquals(viewportRectLeft, other.viewportRectLeft)
- && FloatUtils.fuzzyEquals(viewportRectTop, other.viewportRectTop)
- && viewportRectWidth == other.viewportRectWidth
- && viewportRectHeight == other.viewportRectHeight
- && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
- }
-
- @Override
- public String toString() {
- return "ImmutableViewportMetrics v=(" + viewportRectLeft + "," + viewportRectTop + ","
- + viewportRectWidth + "x" + viewportRectHeight + ") p=(" + pageRectLeft + ","
- + pageRectTop + "," + pageRectRight + "," + pageRectBottom + ") c=("
- + cssPageRectLeft + "," + cssPageRectTop + "," + cssPageRectRight + ","
- + cssPageRectBottom + ") z=" + zoomFactor + ", rtl=" + isRTL;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/IntSize.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public class IntSize {
- public final int width, height;
-
- public IntSize(IntSize size) { width = size.width; height = size.height; }
- public IntSize(int inWidth, int inHeight) { width = inWidth; height = inHeight; }
-
- public IntSize(FloatSize size) {
- width = Math.round(size.width);
- height = Math.round(size.height);
- }
-
- public IntSize(JSONObject json) {
- try {
- width = json.getInt("width");
- height = json.getInt("height");
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- public int getArea() {
- return width * height;
- }
-
- public boolean equals(IntSize size) {
- return ((size.width == width) && (size.height == height));
- }
-
- public boolean isPositive() {
- return (width > 0 && height > 0);
- }
-
- @Override
- public String toString() { return "(" + width + "," + height + ")"; }
-
- public IntSize scale(float factor) {
- return new IntSize(Math.round(width * factor),
- Math.round(height * factor));
- }
-
- /* Returns the power of two that is greater than or equal to value */
- public static int nextPowerOfTwo(int value) {
- // code taken from http://acius2.blogspot.com/2007/11/calculating-next-power-of-2.html
- if (0 == value--) {
- return 1;
- }
- value = (value >> 1) | value;
- value = (value >> 2) | value;
- value = (value >> 4) | value;
- value = (value >> 8) | value;
- value = (value >> 16) | value;
- return value + 1;
- }
-
- public IntSize nextPowerOfTwo() {
- return new IntSize(nextPowerOfTwo(width), nextPowerOfTwo(height));
- }
-
- public static boolean isPowerOfTwo(int value) {
- if (value == 0)
- return false;
- return (value & (value - 1)) == 0;
- }
-
- public static int largestPowerOfTwoLessThan(float value) {
- int val = (int) Math.floor(value);
- if (val <= 0) {
- throw new IllegalArgumentException("Error: value must be > 0");
- }
- // keep dropping the least-significant set bits until only one is left
- int bestVal = val;
- while (val != 0) {
- bestVal = val;
- val &= (val - 1);
- }
- return bestVal;
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/Layer.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.util.FloatUtils;
-
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-
-import java.nio.FloatBuffer;
-import java.util.concurrent.locks.ReentrantLock;
-
-public abstract class Layer {
- private final ReentrantLock mTransactionLock;
- private boolean mInTransaction;
- private Rect mNewPosition;
- private float mNewResolution;
-
- protected Rect mPosition;
- protected float mResolution;
-
- public Layer() {
- this(null);
- }
-
- public Layer(IntSize size) {
- mTransactionLock = new ReentrantLock();
- if (size == null) {
- mPosition = new Rect();
- } else {
- mPosition = new Rect(0, 0, size.width, size.height);
- }
- mResolution = 1.0f;
- }
-
- /**
- * Updates the layer. This returns false if there is still work to be done
- * after this update.
- */
- public final boolean update(RenderContext context) {
- if (mTransactionLock.isHeldByCurrentThread()) {
- throw new RuntimeException("draw() called while transaction lock held by this " +
- "thread?!");
- }
-
- if (mTransactionLock.tryLock()) {
- try {
- performUpdates(context);
- return true;
- } finally {
- mTransactionLock.unlock();
- }
- }
-
- return false;
- }
-
- /** Subclasses override this function to draw the layer. */
- public abstract void draw(RenderContext context);
-
- /** Given the intrinsic size of the layer, returns the pixel boundaries of the layer rect. */
- protected RectF getBounds(RenderContext context) {
- return RectUtils.scale(new RectF(mPosition), context.zoomFactor / mResolution);
- }
-
- /**
- * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer"
- * includes altering the underlying BufferedImage in any way. Thus you must call this function
- * before modifying the byte buffer associated with this layer.
- *
- * This function may block, so you should never call this on the main UI thread.
- */
- public void beginTransaction() {
- if (mTransactionLock.isHeldByCurrentThread())
- throw new RuntimeException("Nested transactions are not supported");
- mTransactionLock.lock();
- mInTransaction = true;
- mNewResolution = mResolution;
- }
-
- /** Call this when you're done modifying the layer. */
- public void endTransaction() {
- if (!mInTransaction)
- throw new RuntimeException("endTransaction() called outside a transaction");
- mInTransaction = false;
- mTransactionLock.unlock();
- }
-
- /** Returns true if the layer is currently in a transaction and false otherwise. */
- protected boolean inTransaction() {
- return mInTransaction;
- }
-
- /** Returns the current layer position. */
- public Rect getPosition() {
- return mPosition;
- }
-
- /** Sets the position. Only valid inside a transaction. */
- public void setPosition(Rect newPosition) {
- if (!mInTransaction)
- throw new RuntimeException("setPosition() is only valid inside a transaction");
- mNewPosition = newPosition;
- }
-
- /** Returns the current layer's resolution. */
- public float getResolution() {
- return mResolution;
- }
-
- /**
- * Sets the layer resolution. This value is used to determine how many pixels per
- * device pixel this layer was rendered at. This will be reflected by scaling by
- * the reciprocal of the resolution in the layer's transform() function.
- * Only valid inside a transaction. */
- public void setResolution(float newResolution) {
- if (!mInTransaction)
- throw new RuntimeException("setResolution() is only valid inside a transaction");
- mNewResolution = newResolution;
- }
-
- /**
- * Subclasses may override this method to perform custom layer updates. This will be called
- * with the transaction lock held. Subclass implementations of this method must call the
- * superclass implementation. Returns false if there is still work to be done after this
- * update is complete.
- */
- protected void performUpdates(RenderContext context) {
- if (mNewPosition != null) {
- mPosition = mNewPosition;
- mNewPosition = null;
- }
- if (mNewResolution != 0.0f) {
- mResolution = mNewResolution;
- mNewResolution = 0.0f;
- }
- }
-
- /**
- * This function fills in the provided <tt>dest</tt> array with values to render a texture.
- * The array is filled with 4 sets of {x, y, z, texture_x, texture_y} values (so 20 values
- * in total) corresponding to the corners of the rect.
- */
- protected final void fillRectCoordBuffer(float[] dest, RectF rect, float viewWidth, float viewHeight,
- Rect cropRect, float texWidth, float texHeight) {
- //x, y, z, texture_x, texture_y
- dest[0] = rect.left / viewWidth;
- dest[1] = rect.bottom / viewHeight;
- dest[2] = 0;
- dest[3] = cropRect.left / texWidth;
- dest[4] = cropRect.top / texHeight;
-
- dest[5] = rect.left / viewWidth;
- dest[6] = rect.top / viewHeight;
- dest[7] = 0;
- dest[8] = cropRect.left / texWidth;
- dest[9] = cropRect.bottom / texHeight;
-
- dest[10] = rect.right / viewWidth;
- dest[11] = rect.bottom / viewHeight;
- dest[12] = 0;
- dest[13] = cropRect.right / texWidth;
- dest[14] = cropRect.top / texHeight;
-
- dest[15] = rect.right / viewWidth;
- dest[16] = rect.top / viewHeight;
- dest[17] = 0;
- dest[18] = cropRect.right / texWidth;
- dest[19] = cropRect.bottom / texHeight;
- }
-
- public static class RenderContext {
- public final RectF viewport;
- public final RectF pageRect;
- public final float zoomFactor;
- public final int positionHandle;
- public final int textureHandle;
- public final FloatBuffer coordBuffer;
-
- public RenderContext(RectF aViewport, RectF aPageRect, float aZoomFactor,
- int aPositionHandle, int aTextureHandle, FloatBuffer aCoordBuffer) {
- viewport = aViewport;
- pageRect = aPageRect;
- zoomFactor = aZoomFactor;
- positionHandle = aPositionHandle;
- textureHandle = aTextureHandle;
- coordBuffer = aCoordBuffer;
- }
-
- public boolean fuzzyEquals(RenderContext other) {
- if (other == null) {
- return false;
- }
- return RectUtils.fuzzyEquals(viewport, other.viewport)
- && RectUtils.fuzzyEquals(pageRect, other.pageRect)
- && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
- }
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/LayerRenderer.java
+++ /dev/null
@@ -1,510 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.gfx.Layer.RenderContext;
-import org.mozilla.gecko.mozglue.DirectBufferAllocator;
-
-import android.graphics.Color;
-import android.graphics.RectF;
-import android.opengl.GLES20;
-import android.util.Log;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.FloatBuffer;
-import java.nio.IntBuffer;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.microedition.khronos.egl.EGLConfig;
-
-/**
- * The layer renderer implements the rendering logic for a layer view.
- */
-public class LayerRenderer {
- private static final String LOGTAG = "GeckoLayerRenderer";
- private static final String PROFTAG = "GeckoLayerRendererProf";
-
- private static final int NANOS_PER_SECOND = 1000000000;
-
- private static final int MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER = 5;
-
- private final LayerView mView;
- private ByteBuffer mCoordByteBuffer;
- private FloatBuffer mCoordBuffer;
- private int mMaxTextureSize;
- private int mBackgroundColor;
-
- private long mLastFrameTime;
- private final CopyOnWriteArrayList<RenderTask> mTasks;
-
- private final CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>();
-
- // Render profiling output
- private int mFramesRendered;
- private float mCompleteFramesRendered;
- private boolean mProfileRender;
- private long mProfileOutputTime;
-
- private IntBuffer mPixelBuffer;
-
- // Used by GLES 2.0
- private int mProgram;
- private int mPositionHandle;
- private int mTextureHandle;
- private int mSampleHandle;
- private int mTMatrixHandle;
-
- private List<LayerView.ZoomedViewListener> mZoomedViewListeners;
- private float mLastViewLeft;
- private float mLastViewTop;
-
- // column-major matrix applied to each vertex to shift the viewport from
- // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by
- // a factor of 2 to fill up the screen
- public static final float[] DEFAULT_TEXTURE_MATRIX = {
- 2.0f, 0.0f, 0.0f, 0.0f,
- 0.0f, 2.0f, 0.0f, 0.0f,
- 0.0f, 0.0f, 2.0f, 0.0f,
- -1.0f, -1.0f, 0.0f, 1.0f
- };
-
- private static final int COORD_BUFFER_SIZE = 20;
-
- // The shaders run on the GPU directly, the vertex shader is only applying the
- // matrix transform detailed above
-
- // Note we flip the y-coordinate in the vertex shader from a
- // coordinate system with (0,0) in the top left to one with (0,0) in
- // the bottom left.
-
- public static final String DEFAULT_VERTEX_SHADER =
- "uniform mat4 uTMatrix;\n" +
- "attribute vec4 vPosition;\n" +
- "attribute vec2 aTexCoord;\n" +
- "varying vec2 vTexCoord;\n" +
- "void main() {\n" +
- " gl_Position = uTMatrix * vPosition;\n" +
- " vTexCoord.x = aTexCoord.x;\n" +
- " vTexCoord.y = 1.0 - aTexCoord.y;\n" +
- "}\n";
-
- // We use highp because the screenshot textures
- // we use are large and we stretch them alot
- // so we need all the precision we can get.
- // Unfortunately, highp is not required by ES 2.0
- // so on GPU's like Mali we end up getting mediump
- public static final String DEFAULT_FRAGMENT_SHADER =
- "precision highp float;\n" +
- "varying vec2 vTexCoord;\n" +
- "uniform sampler2D sTexture;\n" +
- "void main() {\n" +
- " gl_FragColor = texture2D(sTexture, vTexCoord);\n" +
- "}\n";
-
- public LayerRenderer(LayerView view) {
- mView = view;
-
- mTasks = new CopyOnWriteArrayList<RenderTask>();
- mLastFrameTime = System.nanoTime();
-
- mZoomedViewListeners = new ArrayList<LayerView.ZoomedViewListener>();
- }
-
- public void destroy() {
- if (mCoordByteBuffer != null) {
- DirectBufferAllocator.free(mCoordByteBuffer);
- mCoordByteBuffer = null;
- mCoordBuffer = null;
- }
- mZoomedViewListeners.clear();
- }
-
- void onSurfaceCreated(EGLConfig config) {
- checkMonitoringEnabled();
- createDefaultProgram();
- activateDefaultProgram();
- }
-
- public void createDefaultProgram() {
- int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
- int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER);
-
- mProgram = GLES20.glCreateProgram();
- GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
- GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
- GLES20.glLinkProgram(mProgram); // creates OpenGL program executables
-
- // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members.
- mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
- mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
- mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
- mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix");
-
- int maxTextureSizeResult[] = new int[1];
- GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
- mMaxTextureSize = maxTextureSizeResult[0];
- }
-
- // Activates the shader program.
- public void activateDefaultProgram() {
- // Add the program to the OpenGL environment
- GLES20.glUseProgram(mProgram);
-
- // Set the transformation matrix
- GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, DEFAULT_TEXTURE_MATRIX, 0);
-
- // Enable the arrays from which we get the vertex and texture coordinates
- GLES20.glEnableVertexAttribArray(mPositionHandle);
- GLES20.glEnableVertexAttribArray(mTextureHandle);
-
- GLES20.glUniform1i(mSampleHandle, 0);
-
- // TODO: Move these calls into a separate deactivate() call that is called after the
- // underlay and overlay are rendered.
- }
-
- // Deactivates the shader program. This must be done to avoid crashes after returning to the
- // Gecko C++ compositor from Java.
- public void deactivateDefaultProgram() {
- GLES20.glDisableVertexAttribArray(mTextureHandle);
- GLES20.glDisableVertexAttribArray(mPositionHandle);
- GLES20.glUseProgram(0);
- }
-
- void restoreState(boolean enableScissor, int scissorX, int scissorY, int scissorW, int scissorH) {
- GLES20.glScissor(scissorX, scissorY, scissorW, scissorH);
- if (enableScissor) {
- GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
- } else {
- GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
- }
- }
-
- public int getMaxTextureSize() {
- return mMaxTextureSize;
- }
-
- public void postRenderTask(RenderTask aTask) {
- mTasks.add(aTask);
- mView.requestRender();
- }
-
- public void removeRenderTask(RenderTask aTask) {
- mTasks.remove(aTask);
- }
-
- private void runRenderTasks(CopyOnWriteArrayList<RenderTask> tasks, boolean after, long frameStartTime) {
- for (RenderTask task : tasks) {
- if (task.runAfter != after) {
- continue;
- }
-
- boolean stillRunning = task.run(frameStartTime - mLastFrameTime, frameStartTime);
-
- // Remove the task from the list if its finished
- if (!stillRunning) {
- tasks.remove(task);
- }
- }
- }
-
- public void addLayer(Layer layer) {
- synchronized (mExtraLayers) {
- if (mExtraLayers.contains(layer)) {
- mExtraLayers.remove(layer);
- }
-
- mExtraLayers.add(layer);
- }
- }
-
- public void removeLayer(Layer layer) {
- synchronized (mExtraLayers) {
- mExtraLayers.remove(layer);
- }
- }
-
- private void printCheckerboardStats() {
- Log.d(PROFTAG, "Frames rendered over last 1000ms: " + mCompleteFramesRendered + "/" + mFramesRendered);
- mFramesRendered = 0;
- mCompleteFramesRendered = 0;
- }
-
- /** Used by robocop for testing purposes. Not for production use! */
- IntBuffer getPixels() {
- IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight());
- synchronized (pixelBuffer) {
- mPixelBuffer = pixelBuffer;
- mView.requestRender();
- try {
- pixelBuffer.wait();
- } catch (InterruptedException ie) {
- }
- mPixelBuffer = null;
- }
- return pixelBuffer;
- }
-
- private RenderContext createScreenContext(ImmutableViewportMetrics metrics) {
- RectF viewport = new RectF(0.0f, 0.0f, metrics.getWidth(), metrics.getHeight());
- RectF pageRect = metrics.getPageRect();
-
- return createContext(viewport, pageRect, 1.0f);
- }
-
- private RenderContext createPageContext(ImmutableViewportMetrics metrics) {
- RectF viewport = metrics.getViewport();
- RectF pageRect = metrics.getPageRect();
- float zoomFactor = metrics.zoomFactor;
-
- return createContext(new RectF(RectUtils.round(viewport)), pageRect, zoomFactor);
- }
-
- private RenderContext createContext(RectF viewport, RectF pageRect, float zoomFactor) {
- if (mCoordBuffer == null) {
- // Initialize the FloatBuffer that will be used to store all vertices and texture
- // coordinates in draw() commands.
- mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4);
- mCoordByteBuffer.order(ByteOrder.nativeOrder());
- mCoordBuffer = mCoordByteBuffer.asFloatBuffer();
- if (mCoordBuffer == null) {
- throw new IllegalStateException();
- }
- }
- return new RenderContext(viewport, pageRect, zoomFactor,
- mPositionHandle, mTextureHandle, mCoordBuffer);
- }
-
- void checkMonitoringEnabled() {
- mProfileRender = Log.isLoggable(PROFTAG, Log.DEBUG);
- }
-
- /*
- * create a vertex shader type (GLES20.GL_VERTEX_SHADER)
- * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
- */
- public static int loadShader(int type, String shaderCode) {
- int shader = GLES20.glCreateShader(type);
- GLES20.glShaderSource(shader, shaderCode);
- GLES20.glCompileShader(shader);
- return shader;
- }
-
- public Frame createFrame(ImmutableViewportMetrics metrics) {
- return new Frame(metrics);
- }
-
- public class Frame {
- // The timestamp recording the start of this frame.
- private long mFrameStartTime;
- // A fixed snapshot of the viewport metrics that this frame is using to render content.
- private final ImmutableViewportMetrics mFrameMetrics;
- // A rendering context for page-positioned layers, and one for screen-positioned layers.
- private final RenderContext mPageContext, mScreenContext;
- // Whether a layer was updated.
- private boolean mUpdated;
-
- public Frame(ImmutableViewportMetrics metrics) {
- mFrameMetrics = metrics;
-
- // Work out the offset due to margins
- mPageContext = createPageContext(metrics);
- mScreenContext = createScreenContext(metrics);
- }
-
- /** This function is invoked via JNI; be careful when modifying signature. */
- @WrapForJNI(allowMultithread = true)
- public void beginDrawing() {
- mFrameStartTime = System.nanoTime();
-
- TextureReaper.get().reap();
- TextureGenerator.get().fill();
-
- mUpdated = true;
-
- Layer rootLayer = mView.getLayerClient().getRoot();
-
- // Run through pre-render tasks
- runRenderTasks(mTasks, false, mFrameStartTime);
-
- /* Update layers. */
- if (rootLayer != null) {
- // Called on compositor thread.
- mUpdated &= rootLayer.update(mPageContext);
- }
-
- for (Layer layer : mExtraLayers) {
- mUpdated &= layer.update(mPageContext); // called on compositor thread
- }
- }
-
- private void clear(int color) {
- GLES20.glClearColor(((color >> 16) & 0xFF) / 255.0f,
- ((color >> 8) & 0xFF) / 255.0f,
- (color & 0xFF) / 255.0f,
- 0.0f);
- // The bits set here need to match up with those used
- // in gfx/layers/opengl/LayerManagerOGL.cpp.
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
- GLES20.GL_DEPTH_BUFFER_BIT);
- }
-
- /** This function is invoked via JNI; be careful when modifying signature. */
- @WrapForJNI(allowMultithread = true)
- public void drawBackground() {
- // Any GL state which is changed here must be restored in
- // restoreState(...)
-
- GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
-
- // Update background color.
- mBackgroundColor = mView.getBackgroundColor();
-
- // Clear the page area to the page background colour.
- clear(mBackgroundColor);
- }
-
- @WrapForJNI(allowMultithread = true)
- public void drawForeground() {
- // Any GL state which is changed here must be restored in
- // restoreState(...)
-
- /* Draw any extra layers that were added (likely plugins) */
- if (mExtraLayers.size() > 0) {
- for (Layer layer : mExtraLayers) {
- layer.draw(mPageContext);
- }
- }
-
- /* Measure how much of the screen is checkerboarding */
- Layer rootLayer = mView.getLayerClient().getRoot();
- if ((rootLayer != null) &&
- (mProfileRender || PanningPerfAPI.isRecordingCheckerboard())) {
- // Calculate the incompletely rendered area of the page
- float checkerboard = 1.0f - GeckoAppShell.computeRenderIntegrity();
-
- PanningPerfAPI.recordCheckerboard(checkerboard);
- if (checkerboard < 0.0f || checkerboard > 1.0f) {
- Log.e(LOGTAG, "Checkerboard value out of bounds: " + checkerboard);
- }
-
- mCompleteFramesRendered += 1.0f - checkerboard;
- mFramesRendered ++;
-
- if (mFrameStartTime - mProfileOutputTime > NANOS_PER_SECOND) {
- mProfileOutputTime = mFrameStartTime;
- printCheckerboardStats();
- }
- }
-
- runRenderTasks(mTasks, true, mFrameStartTime);
-
- }
-
- private void maybeRequestZoomedViewRender(RenderContext context) {
- // Concurrently update of mZoomedViewListeners should not be an issue here
- // because the following line is just a short-circuit
- if (mZoomedViewListeners.size() == 0) {
- return;
- }
-
- // When scrolling fast, do not request zoomed view render to avoid to slow down
- // the scroll in the main view.
- // Speed is estimated using the offset changes between 2 display frame calls
- final float viewLeft = context.viewport.left;
- final float viewTop = context.viewport.top;
- boolean shouldWaitToRender = false;
-
- if (Math.abs(mLastViewLeft - viewLeft) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER ||
- Math.abs(mLastViewTop - viewTop) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER) {
- shouldWaitToRender = true;
- }
-
- mLastViewLeft = viewLeft;
- mLastViewTop = viewTop;
-
- if (shouldWaitToRender) {
- return;
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- for (LayerView.ZoomedViewListener listener : mZoomedViewListeners) {
- listener.requestZoomedViewRender();
- }
- }
- });
- }
-
- /** This function is invoked via JNI; be careful when modifying signature. */
- @WrapForJNI(allowMultithread = true)
- public void endDrawing() {
- // If a layer update requires further work, schedule another redraw
- if (!mUpdated)
- mView.requestRender();
-
- PanningPerfAPI.recordFrameTime();
-
- maybeRequestZoomedViewRender(mPageContext);
-
- /* Used by robocop for testing purposes */
- IntBuffer pixelBuffer = mPixelBuffer;
- if (mUpdated && pixelBuffer != null) {
- synchronized (pixelBuffer) {
- pixelBuffer.position(0);
- GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(),
- (int)mScreenContext.viewport.height(), GLES20.GL_RGBA,
- GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
- pixelBuffer.notify();
- }
- }
-
- // Remove background color once we've painted. GeckoLayerClient is
- // responsible for setting this flag before current document is
- // composited.
- if (mView.getPaintState() == LayerView.PAINT_BEFORE_FIRST) {
- mView.post(new Runnable() {
- @Override
- public void run() {
- mView.setSurfaceBackgroundColor(Color.TRANSPARENT);
- }
- });
- mView.setPaintState(LayerView.PAINT_AFTER_FIRST);
- }
- mLastFrameTime = mFrameStartTime;
- }
- }
-
- public void updateZoomedView(final ByteBuffer data) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- for (LayerView.ZoomedViewListener listener : mZoomedViewListeners) {
- data.position(0);
- listener.updateView(data);
- }
- }
- });
- }
-
- public void addZoomedViewListener(LayerView.ZoomedViewListener listener) {
- ThreadUtils.assertOnUiThread();
- mZoomedViewListeners.add(listener);
- }
-
- public void removeZoomedViewListener(LayerView.ZoomedViewListener listener) {
- ThreadUtils.assertOnUiThread();
- mZoomedViewListeners.remove(listener);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/LayerView.java
+++ /dev/null
@@ -1,721 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import java.nio.ByteBuffer;
-import java.nio.IntBuffer;
-
-import org.mozilla.gecko.AndroidGamepadManager;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAccessibility;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.GeckoThread;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import android.view.TextureView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.InputDevice;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
-
-/**
- * A view rendered by the layer compositor.
- */
-public class LayerView extends ScrollView {
- private static final String LOGTAG = "GeckoLayerView";
-
- private GeckoLayerClient mLayerClient;
- private PanZoomController mPanZoomController;
- private DynamicToolbarAnimator mToolbarAnimator;
- private GLController mGLController;
- private LayerRenderer mRenderer;
- /* Must be a PAINT_xxx constant */
- private int mPaintState;
- private int mBackgroundColor;
- private FullScreenState mFullScreenState;
-
- private SurfaceView mSurfaceView;
- private TextureView mTextureView;
- private View mFillerView;
-
- private Listener mListener;
-
- private PointF mInitialTouchPoint;
-
- private float mSurfaceTranslation;
-
- /* This should only be modified on the Java UI thread. */
- private final Overscroll mOverscroll;
-
- /* Flags used to determine when to show the painted surface. */
- public static final int PAINT_START = 0;
- public static final int PAINT_BEFORE_FIRST = 1;
- public static final int PAINT_AFTER_FIRST = 2;
-
- public boolean shouldUseTextureView() {
- // Disable TextureView support for now as it causes panning/zooming
- // performance regressions (see bug 792259). Uncomment the code below
- // once this bug is fixed.
- return false;
-
- /*
- // we can only use TextureView on ICS or higher
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- Log.i(LOGTAG, "Not using TextureView: not on ICS+");
- return false;
- }
-
- try {
- // and then we can only use it if we have a hardware accelerated window
- Method m = View.class.getMethod("isHardwareAccelerated", (Class[]) null);
- return (Boolean) m.invoke(this);
- } catch (Exception e) {
- Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString());
- return false;
- } */
- }
-
- public LayerView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mPaintState = PAINT_START;
- mBackgroundColor = Color.WHITE;
- mFullScreenState = FullScreenState.NONE;
-
- if (Versions.feature14Plus) {
- mOverscroll = new OverscrollEdgeEffect(this);
- } else {
- mOverscroll = null;
- }
- }
-
- public LayerView(Context context) {
- this(context, null);
- }
-
- public void initializeView(EventDispatcher eventDispatcher) {
- mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher);
- if (mOverscroll != null) {
- mLayerClient.setOverscrollHandler(mOverscroll);
- }
-
- mPanZoomController = mLayerClient.getPanZoomController();
- mToolbarAnimator = mLayerClient.getDynamicToolbarAnimator();
-
- mRenderer = new LayerRenderer(this);
-
- setFocusable(true);
- setFocusableInTouchMode(true);
-
- GeckoAccessibility.setDelegate(this);
- GeckoAccessibility.setAccessibilityManagerListeners(getContext());
- }
-
- /**
- * MotionEventHelper dragAsync() robocop tests can instruct
- * PanZoomController not to generate longpress events.
- */
- public void setIsLongpressEnabled(boolean isLongpressEnabled) {
- mPanZoomController.setIsLongpressEnabled(isLongpressEnabled);
- }
-
- private static Point getEventRadius(MotionEvent event) {
- return new Point((int)event.getToolMajor() / 2,
- (int)event.getToolMinor() / 2);
- }
-
- private boolean sendEventToGecko(MotionEvent event) {
- if (!mLayerClient.isGeckoReady()) {
- return false;
- }
-
- int action = event.getActionMasked();
- PointF point = new PointF(event.getX(), event.getY());
- if (action == MotionEvent.ACTION_DOWN) {
- mInitialTouchPoint = point;
- }
-
- if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) {
- Point p = getEventRadius(event);
-
- if (PointUtils.subtract(point, mInitialTouchPoint).length() <
- Math.max(PanZoomController.CLICK_THRESHOLD, Math.min(Math.min(p.x, p.y), PanZoomController.PAN_THRESHOLD))) {
- // Don't send the touchmove event if if the users finger hasn't moved far.
- // Necessary for Google Maps to work correctly. See bug 771099.
- return true;
- } else {
- mInitialTouchPoint = null;
- }
- }
-
- GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event, false));
- return true;
- }
-
- public void showSurface() {
- // Fix this if TextureView support is turned back on above
- mSurfaceView.setVisibility(View.VISIBLE);
- }
-
- public void hideSurface() {
- // Fix this if TextureView support is turned back on above
- mSurfaceView.setVisibility(View.INVISIBLE);
- }
-
- public void destroy() {
- if (mLayerClient != null) {
- mLayerClient.destroy();
- }
- if (mRenderer != null) {
- mRenderer.destroy();
- }
- if (mGLController != null) {
- if (mGLController.mView == this) {
- mGLController.mView = null;
- }
- mGLController = null;
- }
- }
-
- @Override
- public void dispatchDraw(final Canvas canvas) {
- super.dispatchDraw(canvas);
-
- // We must have a layer client to get valid viewport metrics
- if (mLayerClient != null && mOverscroll != null) {
- mOverscroll.draw(canvas, getViewportMetrics());
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- requestFocus();
- }
- event.offsetLocation(0, -mSurfaceTranslation);
-
- if (mToolbarAnimator != null && mToolbarAnimator.onInterceptTouchEvent(event)) {
- if (mPanZoomController != null) {
- mPanZoomController.onMotionEventVelocity(event.getEventTime(), mToolbarAnimator.getVelocity());
- }
- return true;
- }
- if (AppConstants.MOZ_ANDROID_APZ && !mLayerClient.isGeckoReady()) {
- // If gecko isn't loaded yet, don't try sending events to the
- // native code because it's just going to crash
- return true;
- }
- if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) {
- return true;
- }
- return sendEventToGecko(event);
- }
-
- @Override
- public boolean onHoverEvent(MotionEvent event) {
- // If we get a touchscreen hover event, and accessibility is not enabled,
- // don't send it to gecko.
- if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN &&
- !GeckoAccessibility.isEnabled()) {
- return false;
- }
-
- event.offsetLocation(0, -mSurfaceTranslation);
-
- if (AppConstants.MOZ_ANDROID_APZ) {
- if (!mLayerClient.isGeckoReady()) {
- // If gecko isn't loaded yet, don't try sending events to the
- // native code because it's just going to crash
- return true;
- } else if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
- return true;
- }
- }
-
- return sendEventToGecko(event);
- }
-
- @Override
- public boolean onGenericMotionEvent(MotionEvent event) {
- event.offsetLocation(0, -mSurfaceTranslation);
-
- if (AndroidGamepadManager.handleMotionEvent(event)) {
- return true;
- }
- if (AppConstants.MOZ_ANDROID_APZ && !mLayerClient.isGeckoReady()) {
- // If gecko isn't loaded yet, don't try sending events to the
- // native code because it's just going to crash
- return true;
- }
- if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
- return true;
- }
- return false;
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- // We are adding descendants to this LayerView, but we don't want the
- // descendants to affect the way LayerView retains its focus.
- setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
-
- // This check should not be done before the view is attached to a window
- // as hardware acceleration will not be enabled at that point.
- // We must create and add the SurfaceView instance before the view tree
- // is fully created to avoid flickering (see bug 801477).
- if (shouldUseTextureView()) {
- mTextureView = new TextureView(getContext());
- mTextureView.setSurfaceTextureListener(new SurfaceTextureListener());
-
- // The background is set to this color when the LayerView is
- // created, and it will be shown immediately at startup. Shortly
- // after, the tab's background color will be used before any content
- // is shown.
- mTextureView.setBackgroundColor(Color.WHITE);
- addView(mTextureView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- } else {
- // This will stop PropertyAnimator from creating a drawing cache (i.e. a bitmap)
- // from a SurfaceView, which is just not possible (the bitmap will be transparent).
- setWillNotCacheDrawing(false);
-
- mSurfaceView = new LayerSurfaceView(getContext(), this);
- mSurfaceView.setBackgroundColor(Color.WHITE);
-
- // The "filler" view sits behind the URL bar and should never be
- // visible. It exists solely to make this LayerView actually
- // scrollable so that we can shift the surface around on the screen.
- // Once we drop support for pre-Honeycomb Android versions this
- // should not be needed; we can just turn LayerView back into a
- // FrameLayout that holds mSurfaceView and nothing else.
- mFillerView = new View(getContext()) {
- @Override protected void onMeasure(int aWidthSpec, int aHeightSpec) {
- setMeasuredDimension(0, Math.round(mToolbarAnimator.getMaxTranslation()));
- }
- };
- mFillerView.setBackgroundColor(Color.RED);
-
- LinearLayout container = new LinearLayout(getContext());
- container.setOrientation(LinearLayout.VERTICAL);
- container.addView(mFillerView);
- container.addView(mSurfaceView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
- addView(container, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
-
- SurfaceHolder holder = mSurfaceView.getHolder();
- holder.addCallback(new SurfaceListener());
- }
- }
-
- // Don't expose GeckoLayerClient to things outside this package; only expose it as an Object
- GeckoLayerClient getLayerClient() { return mLayerClient; }
-
- public PanZoomController getPanZoomController() { return mPanZoomController; }
- public DynamicToolbarAnimator getDynamicToolbarAnimator() { return mToolbarAnimator; }
-
- public ImmutableViewportMetrics getViewportMetrics() {
- return mLayerClient.getViewportMetrics();
- }
-
- public void abortPanning() {
- if (mPanZoomController != null) {
- mPanZoomController.abortPanning();
- }
- }
-
- public PointF convertViewPointToLayerPoint(PointF viewPoint) {
- return mLayerClient.convertViewPointToLayerPoint(viewPoint);
- }
-
- int getBackgroundColor() {
- return mBackgroundColor;
- }
-
- @Override
- public void setBackgroundColor(int newColor) {
- mBackgroundColor = newColor;
- requestRender();
- }
-
- public void setSurfaceBackgroundColor(int newColor) {
- if (mSurfaceView != null) {
- mSurfaceView.setBackgroundColor(newColor);
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (AppConstants.MOZ_ANDROID_APZ && !mLayerClient.isGeckoReady()) {
- // If gecko isn't loaded yet, don't try sending events to the
- // native code because it's just going to crash
- return true;
- }
- if (mPanZoomController != null && mPanZoomController.onKeyEvent(event)) {
- return true;
- }
- return false;
- }
-
- public void requestRender() {
- if (mListener != null) {
- mListener.renderRequested();
- }
- }
-
- public void addLayer(Layer layer) {
- mRenderer.addLayer(layer);
- }
-
- public void removeLayer(Layer layer) {
- mRenderer.removeLayer(layer);
- }
-
- public void postRenderTask(RenderTask task) {
- mRenderer.postRenderTask(task);
- }
-
- public void removeRenderTask(RenderTask task) {
- mRenderer.removeRenderTask(task);
- }
-
- public int getMaxTextureSize() {
- return mRenderer.getMaxTextureSize();
- }
-
- /** Used by robocop for testing purposes. Not for production use! */
- @RobocopTarget
- public IntBuffer getPixels() {
- return mRenderer.getPixels();
- }
-
- /* paintState must be a PAINT_xxx constant. */
- public void setPaintState(int paintState) {
- mPaintState = paintState;
- }
-
- public int getPaintState() {
- return mPaintState;
- }
-
- public LayerRenderer getRenderer() {
- return mRenderer;
- }
-
- public void setListener(Listener listener) {
- mListener = listener;
- }
-
- Listener getListener() {
- return mListener;
- }
-
- public void setGLController(final GLController glController) {
- mGLController = glController;
- glController.mView = this;
-
- final NativePanZoomController npzc = AppConstants.MOZ_ANDROID_APZ ?
- (NativePanZoomController) mPanZoomController : null;
-
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- glController.attachToJava(mLayerClient, npzc);
- } else {
- GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
- glController, "attachToJava",
- GeckoLayerClient.class, mLayerClient,
- NativePanZoomController.class, npzc);
- }
- }
-
- public GLController getGLController() {
- return mGLController;
- }
-
- /* When using a SurfaceView (mSurfaceView != null), resizing happens in two
- * phases. First, the LayerView changes size, then, often some frames later,
- * the SurfaceView changes size. Because of this, we need to split the
- * resize into two phases to avoid jittering.
- *
- * The first phase is the LayerView size change. mListener is notified so
- * that a synchronous draw can be performed (otherwise a blank frame will
- * appear).
- *
- * The second phase is the SurfaceView size change. At this point, the
- * backing GL surface is resized and another synchronous draw is performed.
- * Gecko is also sent the new window size, and this will likely cause an
- * extra draw a few frames later, after it's re-rendered and caught up.
- *
- * In the case that there is no valid GL surface (for example, when
- * resuming, or when coming back from the awesomescreen), or we're using a
- * TextureView instead of a SurfaceView, the first phase is skipped.
- */
- private void onSizeChanged(int width, int height) {
- if (!mGLController.isServerSurfaceValid() || mSurfaceView == null) {
- surfaceChanged(width, height);
- return;
- }
-
- if (mListener != null) {
- mListener.sizeChanged(width, height);
- }
-
- if (mOverscroll != null) {
- mOverscroll.setSize(width, height);
- }
- }
-
- private void surfaceChanged(int width, int height) {
- mGLController.serverSurfaceChanged(width, height);
-
- if (mListener != null) {
- mListener.surfaceChanged(width, height);
- }
-
- if (mOverscroll != null) {
- mOverscroll.setSize(width, height);
- }
- }
-
- private void onDestroyed() {
- mGLController.serverSurfaceDestroyed();
- }
-
- public Object getNativeWindow() {
- if (mSurfaceView != null)
- return mSurfaceView.getHolder();
-
- return mTextureView.getSurfaceTexture();
- }
-
- public Object getSurface() {
- if (mSurfaceView != null) {
- return mSurfaceView.getHolder().getSurface();
- }
- return null;
- }
-
- //This method is called on the Gecko main thread.
- @WrapForJNI(allowMultithread = true, stubName = "updateZoomedView")
- public static void updateZoomedView(ByteBuffer data) {
- LayerView layerView = GeckoAppShell.getLayerView();
- if (layerView != null) {
- LayerRenderer layerRenderer = layerView.getRenderer();
- if (layerRenderer != null) {
- layerRenderer.updateZoomedView(data);
- }
- }
- }
-
- public interface Listener {
- void renderRequested();
- void sizeChanged(int width, int height);
- void surfaceChanged(int width, int height);
- }
-
- private class SurfaceListener implements SurfaceHolder.Callback {
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width,
- int height) {
- onSizeChanged(width, height);
- }
-
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- onDestroyed();
- }
- }
-
- @Override
- protected void onMeasure(int aWidthSpec, int aHeightSpec) {
- super.onMeasure(aWidthSpec, aHeightSpec);
- if (mSurfaceView != null) {
- // Because of the crazy setup where this LayerView is a ScrollView
- // and the SurfaceView is inside a LinearLayout, the SurfaceView
- // doesn't get the right information to size itself the way we want.
- // We always want it to be the same size as this LayerView, so we
- // use a hack to make sure it sizes itself that way.
- ((LayerSurfaceView)mSurfaceView).overrideSize(getMeasuredWidth(), getMeasuredHeight());
- }
- }
-
- /* A subclass of SurfaceView to listen to layout changes, as
- * View.OnLayoutChangeListener requires API level 11.
- */
- private class LayerSurfaceView extends SurfaceView {
- private LayerView mParent;
- private int mForcedWidth;
- private int mForcedHeight;
-
- public LayerSurfaceView(Context aContext, LayerView aParent) {
- super(aContext);
- mParent = aParent;
- }
-
- void overrideSize(int aWidth, int aHeight) {
- if (mForcedWidth != aWidth || mForcedHeight != aHeight) {
- mForcedWidth = aWidth;
- mForcedHeight = aHeight;
- requestLayout();
- }
- }
-
- @Override
- protected void onMeasure(int aWidthSpec, int aHeightSpec) {
- setMeasuredDimension(mForcedWidth, mForcedHeight);
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- if (changed && mParent.mGLController.isServerSurfaceValid()) {
- mParent.surfaceChanged(right - left, bottom - top);
- }
- }
- }
-
- private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
- @Override
- public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
- // We don't do this for surfaceCreated above because it is always followed by a surfaceChanged,
- // but that is not the case here.
- onSizeChanged(width, height);
- }
-
- @Override
- public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
- onDestroyed();
- return true; // allow Android to call release() on the SurfaceTexture, we are done drawing to it
- }
-
- @Override
- public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
- onSizeChanged(width, height);
- }
-
- @Override
- public void onSurfaceTextureUpdated(SurfaceTexture surface) {
-
- }
- }
-
- @RobocopTarget
- public void addDrawListener(DrawListener listener) {
- mLayerClient.addDrawListener(listener);
- }
-
- @RobocopTarget
- public void removeDrawListener(DrawListener listener) {
- mLayerClient.removeDrawListener(listener);
- }
-
- @RobocopTarget
- public static interface DrawListener {
- public void drawFinished();
- }
-
- @Override
- public void setOverScrollMode(int overscrollMode) {
- super.setOverScrollMode(overscrollMode);
- if (mPanZoomController != null) {
- mPanZoomController.setOverScrollMode(overscrollMode);
- }
- }
-
- @Override
- public int getOverScrollMode() {
- if (mPanZoomController != null) {
- return mPanZoomController.getOverScrollMode();
- }
-
- return super.getOverScrollMode();
- }
-
- public float getZoomFactor() {
- return getLayerClient().getViewportMetrics().zoomFactor;
- }
-
- @Override
- public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
- super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- GeckoAccessibility.onLayerViewFocusChanged(this, gainFocus);
- }
-
- public void setFullScreenState(FullScreenState state) {
- mFullScreenState = state;
- }
-
- public boolean isFullScreen() {
- return mFullScreenState != FullScreenState.NONE;
- }
-
- public FullScreenState getFullScreenState() {
- return mFullScreenState;
- }
-
- public void setMaxTranslation(float aMaxTranslation) {
- mToolbarAnimator.setMaxTranslation(aMaxTranslation);
- if (mFillerView != null) {
- mFillerView.requestLayout();
- }
- }
-
- public void setSurfaceTranslation(float translation) {
- // Once we drop support for pre-Honeycomb Android versions, we can
- // revert bug 1197811 and just use ViewHelper here.
- if (mSurfaceTranslation != translation) {
- mSurfaceTranslation = translation;
- scrollTo(0, Math.round(mToolbarAnimator.getMaxTranslation() - translation));
- }
- }
-
- public float getSurfaceTranslation() {
- return mSurfaceTranslation;
- }
-
- // Public hooks for dynamic toolbar translation
-
- public interface DynamicToolbarListener {
- public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation);
- public void onPanZoomStopped();
- public void onMetricsChanged(ImmutableViewportMetrics viewport);
- }
-
- // Public hooks for zoomed view
-
- public interface ZoomedViewListener {
- public void requestZoomedViewRender();
- public void updateView(ByteBuffer data);
- }
-
- public void addZoomedViewListener(ZoomedViewListener listener) {
- mRenderer.addZoomedViewListener(listener);
- }
-
- public void removeZoomedViewListener(ZoomedViewListener listener) {
- mRenderer.removeZoomedViewListener(listener);
- }
-
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/NativePanZoomController.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
-import org.mozilla.gecko.mozglue.JNIObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONObject;
-
-import android.graphics.PointF;
-import android.util.TypedValue;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.InputDevice;
-
-class NativePanZoomController extends JNIObject implements PanZoomController {
- private final PanZoomTarget mTarget;
- private final LayerView mView;
- private boolean mDestroyed;
- private Overscroll mOverscroll;
- boolean mNegateWheelScroll;
- private float mPointerScrollFactor;
- private final PrefsHelper.PrefHandler mPrefsObserver;
- private long mLastDownTime;
- private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
-
- @WrapForJNI
- private native boolean handleMotionEvent(
- int action, int actionIndex, long time, int metaState,
- int pointerId[], float x[], float y[], float orientation[], float pressure[],
- float toolMajor[], float toolMinor[]);
-
- @WrapForJNI
- private native boolean handleScrollEvent(
- long time, int metaState,
- float x, float y,
- float hScroll, float vScroll);
-
- @WrapForJNI
- private native boolean handleMouseEvent(
- int action, long time, int metaState,
- float x, float y, int buttons);
-
- @WrapForJNI
- private native void handleMotionEventVelocity(long time, float ySpeed);
-
- private boolean handleMotionEvent(MotionEvent event) {
- if (mDestroyed) {
- return false;
- }
-
- final int action = event.getActionMasked();
- final int count = event.getPointerCount();
-
- if (action == MotionEvent.ACTION_DOWN) {
- mLastDownTime = event.getDownTime();
- } else if (mLastDownTime != event.getDownTime()) {
- return false;
- }
-
- final int[] pointerId = new int[count];
- final float[] x = new float[count];
- final float[] y = new float[count];
- final float[] orientation = new float[count];
- final float[] pressure = new float[count];
- final float[] toolMajor = new float[count];
- final float[] toolMinor = new float[count];
-
- final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
-
- for (int i = 0; i < count; i++) {
- pointerId[i] = event.getPointerId(i);
- event.getPointerCoords(i, coords);
-
- x[i] = coords.x;
- y[i] = coords.y;
-
- orientation[i] = coords.orientation;
- pressure[i] = coords.pressure;
-
- // If we are converting to CSS pixels, we should adjust the radii as well.
- toolMajor[i] = coords.toolMajor;
- toolMinor[i] = coords.toolMinor;
- }
-
- return handleMotionEvent(action, event.getActionIndex(), event.getEventTime(),
- event.getMetaState(), pointerId, x, y, orientation, pressure,
- toolMajor, toolMinor);
- }
-
- private boolean handleScrollEvent(MotionEvent event) {
- if (mDestroyed) {
- return false;
- }
-
- final int count = event.getPointerCount();
-
- if (count <= 0) {
- return false;
- }
-
- final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
- event.getPointerCoords(0, coords);
- final float x = coords.x;
- final float y = coords.y;
-
- final float flipFactor = mNegateWheelScroll ? -1.0f : 1.0f;
- final float hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) * flipFactor * mPointerScrollFactor;
- final float vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * flipFactor * mPointerScrollFactor;
-
- return handleScrollEvent(event.getEventTime(), event.getMetaState(), x, y, hScroll, vScroll);
- }
-
- private boolean handleMouseEvent(MotionEvent event) {
- if (mDestroyed) {
- return false;
- }
-
- final int count = event.getPointerCount();
-
- if (count <= 0) {
- return false;
- }
-
- final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
- event.getPointerCoords(0, coords);
- final float x = coords.x;
- final float y = coords.y;
-
- return handleMouseEvent(event.getActionMasked(), event.getEventTime(), event.getMetaState(), x, y, event.getButtonState());
- }
-
-
- NativePanZoomController(PanZoomTarget target, View view) {
- mTarget = target;
- mView = (LayerView) view;
-
- String[] prefs = { "ui.scrolling.negate_wheel_scroll" };
- mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
- @Override public void prefValue(String pref, boolean value) {
- if (pref.equals("ui.scrolling.negate_wheel_scroll")) {
- mNegateWheelScroll = value;
- }
- }
- };
- PrefsHelper.addObserver(prefs, mPrefsObserver);
-
- TypedValue outValue = new TypedValue();
- if (view.getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
- mPointerScrollFactor = outValue.getDimension(view.getContext().getResources().getDisplayMetrics());
- } else {
- mPointerScrollFactor = MAX_SCROLL;
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
- return handleMouseEvent(event);
- } else {
- return handleMotionEvent(event);
- }
- }
-
- @Override
- public boolean onMotionEvent(MotionEvent event) {
- final int action = event.getActionMasked();
- if (action == MotionEvent.ACTION_SCROLL) {
- if (event.getDownTime() >= mLastDownTime) {
- mLastDownTime = event.getDownTime();
- } else if ((InputDevice.getDevice(event.getDeviceId()).getSources() & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
- return false;
- }
- return handleScrollEvent(event);
- } else if ((action == MotionEvent.ACTION_HOVER_MOVE) ||
- (action == MotionEvent.ACTION_HOVER_ENTER) ||
- (action == MotionEvent.ACTION_HOVER_EXIT)) {
- return handleMouseEvent(event);
- } else {
- return false;
- }
- }
-
- @Override
- public boolean onKeyEvent(KeyEvent event) {
- // FIXME implement this
- return false;
- }
-
- @Override
- public void onMotionEventVelocity(final long aEventTime, final float aSpeedY) {
- handleMotionEventVelocity(aEventTime, aSpeedY);
- }
-
- @Override
- public PointF getVelocityVector() {
- // FIXME implement this
- return new PointF(0, 0);
- }
-
- @Override
- public void pageRectUpdated() {
- // no-op in APZC, I think
- }
-
- @Override
- public void abortPanning() {
- // no-op in APZC, I think
- }
-
- @Override
- public void notifyDefaultActionPrevented(boolean prevented) {
- // no-op: This could get called if accessibility is enabled and the events
- // are sent to Gecko directly without going through APZ. In this case
- // we just want to ignore this callback.
- }
-
- @WrapForJNI(stubName = "AbortAnimation")
- private native void nativeAbortAnimation();
-
- @Override // PanZoomController
- public void abortAnimation()
- {
- if (!mDestroyed) {
- nativeAbortAnimation();
- }
- }
-
- @Override // PanZoomController
- public boolean getRedrawHint()
- {
- // FIXME implement this
- return true;
- }
-
- @Override @WrapForJNI(allowMultithread = true) // PanZoomController
- public void destroy() {
- if (mDestroyed) {
- return;
- }
- mDestroyed = true;
- disposeNative();
- }
-
- @Override @WrapForJNI // JNIObject
- protected native void disposeNative();
-
- @Override
- public void setOverScrollMode(int overscrollMode) {
- // FIXME implement this
- }
-
- @Override
- public int getOverScrollMode() {
- // FIXME implement this
- return 0;
- }
-
- @WrapForJNI(allowMultithread = true, stubName = "RequestContentRepaintWrapper")
- private void requestContentRepaint(float x, float y, float width, float height, float resolution) {
- mTarget.forceRedraw(new DisplayPortMetrics(x, y, x + width, y + height, resolution));
- }
-
- @Override
- public void setOverscrollHandler(final Overscroll handler) {
- mOverscroll = handler;
- }
-
- @WrapForJNI(stubName = "SetIsLongpressEnabled")
- private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
-
- @Override // PanZoomController
- public void setIsLongpressEnabled(boolean isLongpressEnabled) {
- if (!mDestroyed) {
- nativeSetIsLongpressEnabled(isLongpressEnabled);
- }
- }
-
- @WrapForJNI(stubName = "AdjustScrollForSurfaceShift")
- private native void adjustScrollForSurfaceShift(float aX, float aY);
-
- @Override // PanZoomController
- public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift) {
- adjustScrollForSurfaceShift(aShift.x, aShift.y);
- return aMetrics.offsetViewportByAndClamp(aShift.x, aShift.y);
- }
-
- @WrapForJNI(allowMultithread = true)
- private void updateOverscrollVelocity(final float x, final float y) {
- if (mOverscroll != null) {
- if (ThreadUtils.isOnUiThread() == true) {
- mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
- mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
- } else {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // Multiply the velocity by 1000 to match what was done in JPZ.
- mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
- mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
- }
- });
- }
- }
- }
-
- @WrapForJNI(allowMultithread = true)
- private void updateOverscrollOffset(final float x, final float y) {
- if (mOverscroll != null) {
- if (ThreadUtils.isOnUiThread() == true) {
- mOverscroll.setDistance(x, Overscroll.Axis.X);
- mOverscroll.setDistance(y, Overscroll.Axis.Y);
- } else {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mOverscroll.setDistance(x, Overscroll.Axis.X);
- mOverscroll.setDistance(y, Overscroll.Axis.Y);
- }
- });
- }
- }
- }
-
- @WrapForJNI
- private void setScrollingRootContent(final boolean isRootContent) {
- mTarget.setScrollingRootContent(isRootContent);
- }
-
- /**
- * Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
- * to avoid unwanted scroll interactions.
- */
- @WrapForJNI
- private void onSelectionDragState(boolean state) {
- mView.getDynamicToolbarAnimator().setPinned(state, PinReason.CARET_DRAG);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/Overscroll.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.graphics.Canvas;
-
-public interface Overscroll {
- // The axis to show overscroll on.
- public enum Axis {
- X,
- Y,
- };
-
- public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics);
- public void setSize(final int width, final int height);
- public void setVelocity(final float velocity, final Axis axis);
- public void setDistance(final float distance, final Axis axis);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PointF;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.widget.EdgeEffect;
-
-import java.lang.reflect.Field;
-
-public class OverscrollEdgeEffect implements Overscroll {
- // Used to index particular edges in the edges array
- private static final int TOP = 0;
- private static final int BOTTOM = 1;
- private static final int LEFT = 2;
- private static final int RIGHT = 3;
-
- // All four edges of the screen
- private final EdgeEffect[] mEdges = new EdgeEffect[4];
-
- // The view we're showing this overscroll on.
- private final LayerView mView;
-
- public OverscrollEdgeEffect(final LayerView v) {
- Field paintField = null;
- if (Versions.feature21Plus) {
- try {
- paintField = EdgeEffect.class.getDeclaredField("mPaint");
- paintField.setAccessible(true);
- } catch (NoSuchFieldException e) {
- }
- }
-
- mView = v;
- Context context = v.getContext();
- for (int i = 0; i < 4; i++) {
- mEdges[i] = new EdgeEffect(context);
-
- try {
- if (paintField != null) {
- final Paint p = (Paint) paintField.get(mEdges[i]);
-
- // The Android EdgeEffect class uses a mode of SRC_ATOP here, which means it will only
- // draw the effect where there are non-transparent pixels in the destination. Since the LayerView
- // itself is fully transparent, it doesn't display at all. We need to use SRC instead.
- p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
- }
- } catch (IllegalAccessException e) {
- }
- }
- }
-
- @Override
- public void setSize(final int width, final int height) {
- mEdges[LEFT].setSize(height, width);
- mEdges[RIGHT].setSize(height, width);
- mEdges[TOP].setSize(width, height);
- mEdges[BOTTOM].setSize(width, height);
- }
-
- private EdgeEffect getEdgeForAxisAndSide(final Axis axis, final float side) {
- if (axis == Axis.Y) {
- if (side < 0) {
- return mEdges[TOP];
- } else {
- return mEdges[BOTTOM];
- }
- } else {
- if (side < 0) {
- return mEdges[LEFT];
- } else {
- return mEdges[RIGHT];
- }
- }
- }
-
- private void invalidate() {
- if (Versions.feature16Plus) {
- mView.postInvalidateOnAnimation();
- } else {
- mView.postInvalidateDelayed(10);
- }
- }
-
- @Override
- public void setVelocity(final float velocity, final Axis axis) {
- final EdgeEffect edge = getEdgeForAxisAndSide(axis, velocity);
-
- // If we're showing overscroll already, start fading it out.
- if (!edge.isFinished()) {
- edge.onRelease();
- } else {
- // Otherwise, show an absorb effect
- edge.onAbsorb((int)velocity);
- }
-
- invalidate();
- }
-
- @Override
- public void setDistance(final float distance, final Axis axis) {
- // The first overscroll event often has zero distance. Throw it out
- if (distance == 0.0f) {
- return;
- }
-
- final EdgeEffect edge = getEdgeForAxisAndSide(axis, (int)distance);
- edge.onPull(distance / (axis == Axis.X ? mView.getWidth() : mView.getHeight()));
- invalidate();
- }
-
- @Override
- public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics) {
- if (metrics == null) {
- return;
- }
-
- float fillerSize = mView.getDynamicToolbarAnimator().getMaxTranslation();
- PointF visibleEnd = mView.getDynamicToolbarAnimator().getVisibleEndOfLayerView();
-
- // If we're pulling an edge, or fading it out, draw!
- boolean invalidate = false;
- if (!mEdges[TOP].isFinished()) {
- invalidate |= draw(mEdges[TOP], canvas, 0, fillerSize, 0);
- }
-
- if (!mEdges[BOTTOM].isFinished()) {
- invalidate |= draw(mEdges[BOTTOM], canvas, visibleEnd.x, fillerSize + visibleEnd.y, 180);
- }
-
- if (!mEdges[LEFT].isFinished()) {
- invalidate |= draw(mEdges[LEFT], canvas, 0, fillerSize + visibleEnd.y, 270);
- }
-
- if (!mEdges[RIGHT].isFinished()) {
- invalidate |= draw(mEdges[RIGHT], canvas, visibleEnd.x, fillerSize, 90);
- }
-
- // If the edge effect is animating off screen, invalidate.
- if (invalidate) {
- invalidate();
- }
- }
-
- private static boolean draw(final EdgeEffect edge, final Canvas canvas, final float translateX, final float translateY, final float rotation) {
- final int state = canvas.save();
- canvas.translate(translateX, translateY);
- canvas.rotate(rotation);
- boolean invalidate = edge.draw(canvas);
- canvas.restoreToCount(state);
-
- return invalidate;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PanZoomController.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.graphics.PointF;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-
-public interface PanZoomController {
- // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
- // between the touch-down and touch-up of a click). In units of density-independent pixels.
- public static final float PAN_THRESHOLD = 1 / 16f * GeckoAppShell.getDpi();
-
- // Threshold for sending touch move events to content
- public static final float CLICK_THRESHOLD = 1 / 50f * GeckoAppShell.getDpi();
-
- static class Factory {
- static PanZoomController create(PanZoomTarget target, View view, EventDispatcher dispatcher) {
- if (org.mozilla.gecko.AppConstants.MOZ_ANDROID_APZ) {
- return new NativePanZoomController(target, view);
- } else {
- throw new IllegalStateException("Must set MOZ_ANDROID_APZ");
- }
- }
- }
-
- public void destroy();
-
- public boolean onTouchEvent(MotionEvent event);
- public boolean onMotionEvent(MotionEvent event);
- public boolean onKeyEvent(KeyEvent event);
- public void onMotionEventVelocity(final long aEventTime, final float aSpeedY);
- public void notifyDefaultActionPrevented(boolean prevented);
-
- public boolean getRedrawHint();
- public PointF getVelocityVector();
-
- public void pageRectUpdated();
- public void abortPanning();
- public void abortAnimation();
-
- public void setOverScrollMode(int overscrollMode);
- public int getOverScrollMode();
-
- public void setOverscrollHandler(final Overscroll controller);
-
- public void setIsLongpressEnabled(boolean isLongpressEnabled);
-
- public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PanZoomTarget.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.graphics.PointF;
-import android.graphics.RectF;
-
-public interface PanZoomTarget {
- public ImmutableViewportMetrics getViewportMetrics();
- public FullScreenState getFullScreenState();
- public PointF getVisibleEndOfLayerView();
-
- public void setAnimationTarget(ImmutableViewportMetrics viewport);
- public void setViewportMetrics(ImmutableViewportMetrics viewport);
- public void scrollBy(float dx, float dy);
- public void panZoomStopped();
- /** This triggers an (asynchronous) viewport update/redraw. */
- public void forceRedraw(DisplayPortMetrics displayPort);
-
- public boolean post(Runnable action);
- public void postRenderTask(RenderTask task);
- public void removeRenderTask(RenderTask task);
- public Object getLock();
- public PointF convertViewPointToLayerPoint(PointF viewPoint);
- public void setScrollingRootContent(boolean isRootContent);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PanningPerfAPI {
- private static final String LOGTAG = "GeckoPanningPerfAPI";
-
- // make this large enough to avoid having to resize the frame time
- // list, as that may be expensive and impact the thing we're trying
- // to measure.
- private static final int EXPECTED_FRAME_COUNT = 2048;
-
- private static boolean mRecordingFrames;
- private static List<Long> mFrameTimes;
- private static long mFrameStartTime;
-
- private static boolean mRecordingCheckerboard;
- private static List<Float> mCheckerboardAmounts;
- private static long mCheckerboardStartTime;
-
- private static void initialiseRecordingArrays() {
- if (mFrameTimes == null) {
- mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
- } else {
- mFrameTimes.clear();
- }
- if (mCheckerboardAmounts == null) {
- mCheckerboardAmounts = new ArrayList<Float>(EXPECTED_FRAME_COUNT);
- } else {
- mCheckerboardAmounts.clear();
- }
- }
-
- @RobocopTarget
- public static void startFrameTimeRecording() {
- if (mRecordingFrames || mRecordingCheckerboard) {
- Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
- return;
- }
- mRecordingFrames = true;
- initialiseRecordingArrays();
- mFrameStartTime = SystemClock.uptimeMillis();
- }
-
- @RobocopTarget
- public static List<Long> stopFrameTimeRecording() {
- if (!mRecordingFrames) {
- Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
- return null;
- }
- mRecordingFrames = false;
- return mFrameTimes;
- }
-
- public static void recordFrameTime() {
- // this will be called often, so try to make it as quick as possible
- if (mRecordingFrames) {
- mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
- }
- }
-
- public static boolean isRecordingCheckerboard() {
- return mRecordingCheckerboard;
- }
-
- @RobocopTarget
- public static void startCheckerboardRecording() {
- if (mRecordingCheckerboard || mRecordingFrames) {
- Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!");
- return;
- }
- mRecordingCheckerboard = true;
- initialiseRecordingArrays();
- mCheckerboardStartTime = SystemClock.uptimeMillis();
- }
-
- @RobocopTarget
- public static List<Float> stopCheckerboardRecording() {
- if (!mRecordingCheckerboard) {
- Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!");
- return null;
- }
- mRecordingCheckerboard = false;
-
- // We take the number of values in mCheckerboardAmounts here, as there's
- // the possibility that this function is called while recordCheckerboard
- // is still executing. As values are added to this list last, we use
- // this number as the canonical number of recordings.
- int values = mCheckerboardAmounts.size();
- if (values == 0) {
- Log.w(LOGTAG, "stopCheckerboardRecording() found no checkerboard amounts!");
- return mCheckerboardAmounts;
- }
-
- // The score will be the sum of all the values in mCheckerboardAmounts,
- // so weight the checkerboard values by time so that frame-rate and
- // run-length don't affect score.
- long lastTime = 0;
- float totalTime = mFrameTimes.get(values - 1);
- for (int i = 0; i < values; i++) {
- long elapsedTime = mFrameTimes.get(i) - lastTime;
- mCheckerboardAmounts.set(i, mCheckerboardAmounts.get(i) * elapsedTime / totalTime);
- lastTime += elapsedTime;
- }
-
- return mCheckerboardAmounts;
- }
-
- public static void recordCheckerboard(float amount) {
- // this will be called often, so try to make it as quick as possible
- if (mRecordingCheckerboard) {
- mFrameTimes.add(SystemClock.uptimeMillis() - mCheckerboardStartTime);
- mCheckerboardAmounts.add(amount);
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PluginLayer.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/* 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.gfx;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.util.FloatUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.view.SurfaceView;
-import android.view.View;
-import android.widget.AbsoluteLayout;
-
-public class PluginLayer extends Layer {
- private static final String LOGTAG = "PluginLayer";
-
- private final View mView;
- private SurfaceView mSurfaceView;
- private final PluginLayoutParams mLayoutParams;
- private final AbsoluteLayout mContainer;
-
- private boolean mDestroyed;
- private boolean mViewVisible;
-
- private RectF mLastViewport;
- private float mLastZoomFactor;
-
- private static final float TEXTURE_MAP[] = {
- 0.0f, 1.0f, // top left
- 0.0f, 0.0f, // bottom left
- 1.0f, 1.0f, // top right
- 1.0f, 0.0f, // bottom right
- };
-
- public PluginLayer(View view, RectF rect, int maxDimension) {
- super(new IntSize(0, 0));
-
- mView = view;
- mContainer = GeckoAppShell.getGeckoInterface().getPluginContainer();
-
- mView.setWillNotDraw(false);
- if (mView instanceof SurfaceView) {
- mSurfaceView = (SurfaceView)view;
- mSurfaceView.setZOrderOnTop(false);
- mSurfaceView.setZOrderMediaOverlay(true);
- }
-
- mLayoutParams = new PluginLayoutParams(rect, maxDimension);
- }
-
- public void setVisible(boolean visible) {
- if (visible) {
- showView();
- } else {
- hideView();
- }
- }
-
- private void hideView() {
- if (mViewVisible) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mView.setVisibility(View.GONE);
- mViewVisible = false;
- }
- });
- }
- }
-
- public void showView() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (mContainer.indexOfChild(mView) < 0) {
- mContainer.addView(mView, mLayoutParams);
- } else {
- mContainer.updateViewLayout(mView, mLayoutParams);
- mView.setVisibility(View.VISIBLE);
- }
- mViewVisible = true;
- }
- });
- }
-
- public void destroy() {
- mDestroyed = true;
-
- mContainer.removeView(mView);
- }
-
- public void reset(RectF rect) {
- mLayoutParams.reset(rect);
- }
-
- @Override
- protected void performUpdates(RenderContext context) {
- if (mDestroyed)
- return;
-
- if (!RectUtils.fuzzyEquals(context.viewport, mLastViewport) ||
- !FloatUtils.fuzzyEquals(context.zoomFactor, mLastZoomFactor)) {
-
- mLastZoomFactor = context.zoomFactor;
- mLastViewport = context.viewport;
- mLayoutParams.reposition(context.viewport, context.zoomFactor);
-
- showView();
- }
- }
-
- @Override
- public void draw(RenderContext context) {
- }
-
- class PluginLayoutParams extends AbsoluteLayout.LayoutParams
- {
- private static final String LOGTAG = "GeckoApp.PluginLayoutParams";
-
- private RectF mRect;
- private final int mMaxDimension;
- private float mLastResolution;
-
- public PluginLayoutParams(RectF rect, int maxDimension) {
- super(0, 0, 0, 0);
-
- mMaxDimension = maxDimension;
- reset(rect);
- }
-
- private void clampToMaxSize() {
- if (width > mMaxDimension || height > mMaxDimension) {
- if (width > height) {
- height = Math.round(((float) height / width) * mMaxDimension);
- width = mMaxDimension;
- } else {
- width = Math.round(((float) width / height) * mMaxDimension);
- height = mMaxDimension;
- }
- }
- }
-
- public void reset(RectF rect) {
- mRect = rect;
- }
-
- public void reposition(RectF viewport, float zoomFactor) {
-
- RectF scaled = RectUtils.scale(mRect, zoomFactor);
-
- this.x = Math.round(scaled.left - viewport.left);
- this.y = Math.round(scaled.top - viewport.top);
-
- if (!FloatUtils.fuzzyEquals(mLastResolution, zoomFactor)) {
- width = Math.round(mRect.width() * zoomFactor);
- height = Math.round(mRect.height() * zoomFactor);
- mLastResolution = zoomFactor;
-
- clampToMaxSize();
- }
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PointUtils.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.graphics.Point;
-import android.graphics.PointF;
-
-public final class PointUtils {
- public static PointF add(PointF one, PointF two) {
- return new PointF(one.x + two.x, one.y + two.y);
- }
-
- public static PointF subtract(PointF one, PointF two) {
- return new PointF(one.x - two.x, one.y - two.y);
- }
-
- public static PointF scale(PointF point, float factor) {
- return new PointF(point.x * factor, point.y * factor);
- }
-
- public static Point round(PointF point) {
- return new Point(Math.round(point.x), Math.round(point.y));
- }
-
- /* Computes the magnitude of the given vector. */
- public static float distance(PointF point) {
- return (float)Math.sqrt(point.x * point.x + point.y * point.y);
- }
-
- /** Computes the scalar distance between two points. */
- public static float distance(PointF one, PointF two) {
- return PointF.length(one.x - two.x, one.y - two.y);
- }
-
- public static JSONObject toJSON(PointF point) throws JSONException {
- // Ensure we put ints, not longs, because Gecko message handlers call getInt().
- int x = Math.round(point.x);
- int y = Math.round(point.y);
- JSONObject json = new JSONObject();
- json.put("x", x);
- json.put("y", y);
- return json;
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-/**
- * This is the data structure that's returned by the progressive tile update
- * callback function. It encompasses the current viewport and a boolean value
- * representing whether the front-end is interested in the current progressive
- * update continuing.
- */
-@WrapForJNI
-public class ProgressiveUpdateData {
- public float x;
- public float y;
- public float scale;
- public boolean abort;
-
- public void setViewport(ImmutableViewportMetrics viewport) {
- this.x = viewport.viewportRectLeft;
- this.y = viewport.viewportRectTop;
- this.scale = viewport.zoomFactor;
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/RectUtils.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.util.FloatUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-
-public final class RectUtils {
- private RectUtils() {}
-
- public static Rect create(JSONObject json) {
- try {
- int x = json.getInt("x");
- int y = json.getInt("y");
- int width = json.getInt("width");
- int height = json.getInt("height");
- return new Rect(x, y, x + width, y + height);
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- public static String toJSON(RectF rect) {
- StringBuilder sb = new StringBuilder(256);
- sb.append("{ \"left\": ").append(rect.left)
- .append(", \"top\": ").append(rect.top)
- .append(", \"right\": ").append(rect.right)
- .append(", \"bottom\": ").append(rect.bottom)
- .append('}');
- return sb.toString();
- }
-
- public static RectF expand(RectF rect, float moreWidth, float moreHeight) {
- float halfMoreWidth = moreWidth / 2;
- float halfMoreHeight = moreHeight / 2;
- return new RectF(rect.left - halfMoreWidth,
- rect.top - halfMoreHeight,
- rect.right + halfMoreWidth,
- rect.bottom + halfMoreHeight);
- }
-
- public static RectF contract(RectF rect, float lessWidth, float lessHeight) {
- float halfLessWidth = lessWidth / 2.0f;
- float halfLessHeight = lessHeight / 2.0f;
- return new RectF(rect.left + halfLessWidth,
- rect.top + halfLessHeight,
- rect.right - halfLessWidth,
- rect.bottom - halfLessHeight);
- }
-
- public static RectF intersect(RectF one, RectF two) {
- float left = Math.max(one.left, two.left);
- float top = Math.max(one.top, two.top);
- float right = Math.min(one.right, two.right);
- float bottom = Math.min(one.bottom, two.bottom);
- return new RectF(left, top, Math.max(right, left), Math.max(bottom, top));
- }
-
- public static RectF scale(RectF rect, float scale) {
- float x = rect.left * scale;
- float y = rect.top * scale;
- return new RectF(x, y,
- x + (rect.width() * scale),
- y + (rect.height() * scale));
- }
-
- public static RectF scaleAndRound(RectF rect, float scale) {
- float left = rect.left * scale;
- float top = rect.top * scale;
- return new RectF(Math.round(left),
- Math.round(top),
- Math.round(left + (rect.width() * scale)),
- Math.round(top + (rect.height() * scale)));
- }
-
- /** Returns the nearest integer rect of the given rect. */
- public static Rect round(RectF rect) {
- Rect r = new Rect();
- round(rect, r);
- return r;
- }
-
- public static void round(RectF rect, Rect dest) {
- dest.set(Math.round(rect.left), Math.round(rect.top),
- Math.round(rect.right), Math.round(rect.bottom));
- }
-
- public static Rect roundIn(RectF rect) {
- return new Rect((int)Math.ceil(rect.left), (int)Math.ceil(rect.top),
- (int)Math.floor(rect.right), (int)Math.floor(rect.bottom));
- }
-
- public static IntSize getSize(Rect rect) {
- return new IntSize(rect.width(), rect.height());
- }
-
- public static Point getOrigin(Rect rect) {
- return new Point(rect.left, rect.top);
- }
-
- public static PointF getOrigin(RectF rect) {
- return new PointF(rect.left, rect.top);
- }
-
- public static boolean fuzzyEquals(RectF a, RectF b) {
- if (a == null && b == null)
- return true;
- else if ((a == null && b != null) || (a != null && b == null))
- return false;
- else
- return FloatUtils.fuzzyEquals(a.top, b.top)
- && FloatUtils.fuzzyEquals(a.left, b.left)
- && FloatUtils.fuzzyEquals(a.right, b.right)
- && FloatUtils.fuzzyEquals(a.bottom, b.bottom);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/RenderTask.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-/**
- * A class used to schedule a callback to occur when the next frame is drawn.
- * Subclasses must redefine the internalRun method, not the run method.
- */
-public abstract class RenderTask {
- /**
- * Whether to run the task after the render, or before.
- */
- public final boolean runAfter;
-
- /**
- * Time when this task has first run, in ns. Useful for tasks which run for a specific duration.
- */
- private long mStartTime;
-
- /**
- * Whether we should initialise mStartTime on the next frame run.
- */
- private boolean mResetStartTime = true;
-
- /**
- * The callback to run on each frame. timeDelta is the time elapsed since
- * the last call, in nanoseconds. Returns true if it should continue
- * running, or false if it should be removed from the task queue. Returning
- * true implicitly schedules a redraw.
- *
- * This method first initializes the start time if resetStartTime has been invoked,
- * then calls internalRun.
- *
- * Note : subclasses should override internalRun.
- *
- * @param timeDelta the time between the beginning of last frame and the beginning of this frame, in ns.
- * @param currentFrameStartTime the startTime of the current frame, in ns.
- * @return true if animation should be run at the next frame, false otherwise
- * @see RenderTask#internalRun(long, long)
- */
- public final boolean run(long timeDelta, long currentFrameStartTime) {
- if (mResetStartTime) {
- mStartTime = currentFrameStartTime;
- mResetStartTime = false;
- }
- return internalRun(timeDelta, currentFrameStartTime);
- }
-
- /**
- * Abstract method to be overridden by subclasses.
- * @param timeDelta the time between the beginning of last frame and the beginning of this frame, in ns
- * @param currentFrameStartTime the startTime of the current frame, in ns.
- * @return true if animation should be run at the next frame, false otherwise
- */
- protected abstract boolean internalRun(long timeDelta, long currentFrameStartTime);
-
- public RenderTask(boolean aRunAfter) {
- runAfter = aRunAfter;
- }
-
- /**
- * Get the start time of this task.
- * It is the start time of the first frame this task was run on.
- * @return the start time in ns
- */
- public long getStartTime() {
- return mStartTime;
- }
-
- /**
- * Schedule a reset of the recorded start time next time {@link RenderTask#run(long, long)} is run.
- * @see RenderTask#getStartTime()
- */
- public void resetStartTime() {
- mResetStartTime = true;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/TextureGenerator.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.opengl.GLES20;
-import android.util.Log;
-
-import java.util.concurrent.ArrayBlockingQueue;
-
-import javax.microedition.khronos.egl.EGL10;
-import javax.microedition.khronos.egl.EGLContext;
-
-public class TextureGenerator {
- private static final String LOGTAG = "TextureGenerator";
- private static final int POOL_SIZE = 5;
-
- private static TextureGenerator sSharedInstance;
-
- private final ArrayBlockingQueue<Integer> mTextureIds;
- private EGLContext mContext;
-
- private TextureGenerator() { mTextureIds = new ArrayBlockingQueue<Integer>(POOL_SIZE); }
-
- public static TextureGenerator get() {
- if (sSharedInstance == null)
- sSharedInstance = new TextureGenerator();
- return sSharedInstance;
- }
-
- public synchronized int take() {
- try {
- // Will block until one becomes available
- return (int)mTextureIds.take();
- } catch (InterruptedException e) {
- return 0;
- }
- }
-
- public synchronized void fill() {
- EGL10 egl = (EGL10)EGLContext.getEGL();
- EGLContext context = egl.eglGetCurrentContext();
-
- if (mContext != null && mContext != context) {
- mTextureIds.clear();
- }
-
- mContext = context;
-
- int numNeeded = mTextureIds.remainingCapacity();
- if (numNeeded == 0)
- return;
-
- // Clear existing GL errors
- int error;
- while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
- Log.w(LOGTAG, String.format("Clearing GL error: %#x", error));
- }
-
- int[] textures = new int[numNeeded];
- GLES20.glGenTextures(numNeeded, textures, 0);
-
- error = GLES20.glGetError();
- if (error != GLES20.GL_NO_ERROR) {
- Log.e(LOGTAG, String.format("Failed to generate textures: %#x", error), new Exception());
- return;
- }
-
- for (int i = 0; i < numNeeded; i++) {
- mTextureIds.offer(textures[i]);
- }
- }
-}
-
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/TextureReaper.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.opengl.GLES20;
-
-import java.util.ArrayList;
-
-/** Manages a list of dead tiles, so we don't leak resources. */
-public class TextureReaper {
- private static TextureReaper sSharedInstance;
- private final ArrayList<Integer> mDeadTextureIDs;
-
- private TextureReaper() { mDeadTextureIDs = new ArrayList<Integer>(); }
-
- public static TextureReaper get() {
- if (sSharedInstance == null)
- sSharedInstance = new TextureReaper();
- return sSharedInstance;
- }
-
- public void add(int[] textureIDs) {
- for (int textureID : textureIDs)
- add(textureID);
- }
-
- public void add(int textureID) {
- mDeadTextureIDs.add(textureID);
- }
-
- public void reap() {
- int numTextures = mDeadTextureIDs.size();
- // Adreno 200 will generate INVALID_VALUE if len == 0 is passed to glDeleteTextures,
- // even though it's not supposed to.
- if (numTextures == 0)
- return;
-
- int[] deadTextureIDs = new int[numTextures];
- for (int i = 0; i < numTextures; i++) {
- deadTextureIDs[i] = mDeadTextureIDs.get(i);
- }
- mDeadTextureIDs.clear();
-
- GLES20.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0);
- }
-}
-
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/ViewTransform.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-@WrapForJNI
-public class ViewTransform {
- public float x;
- public float y;
- public float width;
- public float height;
- public float scale;
- public float fixedLayerMarginLeft;
- public float fixedLayerMarginTop;
- public float fixedLayerMarginRight;
- public float fixedLayerMarginBottom;
-
- public ViewTransform(float inX, float inY, float inScale) {
- x = inX;
- y = inY;
- scale = inScale;
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/VirtualLayer.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-public class VirtualLayer extends Layer {
- public VirtualLayer(IntSize size) {
- super(size);
- }
-
- @Override
- public void draw(RenderContext context) {
- // No-op.
- }
-
- void setPositionAndResolution(int left, int top, int right, int bottom, float newResolution) {
- // This is an optimized version of the following code:
- // beginTransaction();
- // try {
- // setPosition(new Rect(left, top, right, bottom));
- // setResolution(newResolution);
- // performUpdates(null);
- // } finally {
- // endTransaction();
- // }
-
- // it is safe to drop the transaction lock in this instance (i.e. for the
- // VirtualLayer that is just a shadow of what gecko is painting) because
- // the position and resolution of this layer are always touched on the compositor
- // thread, and therefore do not require synchronization.
- mPosition.set(left, top, right, bottom);
- mResolution = newResolution;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/* -*- 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.mozglue;
-
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-
-class ByteBufferInputStream extends InputStream {
-
- protected ByteBuffer mBuf;
- // Reference to a native object holding the data backing the ByteBuffer.
- private final NativeReference mNativeRef;
-
- protected ByteBufferInputStream(ByteBuffer buffer, NativeReference ref) {
- mBuf = buffer;
- mNativeRef = ref;
- }
-
- @Override
- public int available() {
- return mBuf.remaining();
- }
-
- @Override
- public void close() {
- mBuf = null;
- mNativeRef.release();
- }
-
- @Override
- public int read() {
- if (!mBuf.hasRemaining() || mNativeRef.isReleased()) {
- return -1;
- }
-
- return mBuf.get() & 0xff; // Avoid sign extension
- }
-
- @Override
- public int read(byte[] buffer, int offset, int length) {
- if (!mBuf.hasRemaining() || mNativeRef.isReleased()) {
- return -1;
- }
-
- length = Math.min(length, mBuf.remaining());
- mBuf.get(buffer, offset, length);
- return length;
- }
-
- @Override
- public long skip(long byteCount) {
- if (byteCount < 0 || mNativeRef.isReleased()) {
- return 0;
- }
-
- byteCount = Math.min(byteCount, mBuf.remaining());
- mBuf.position(mBuf.position() + (int)byteCount);
- return byteCount;
- }
-
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.mozglue;
-
-import java.nio.ByteBuffer;
-
-//
-// We must manually allocate direct buffers in JNI to work around a bug where Honeycomb's
-// ByteBuffer.allocateDirect() grossly overallocates the direct buffer size.
-// https://code.google.com/p/android/issues/detail?id=16941
-//
-
-public final class DirectBufferAllocator {
- private DirectBufferAllocator() {}
-
- public static ByteBuffer allocate(int size) {
- if (size <= 0) {
- throw new IllegalArgumentException("Invalid size " + size);
- }
-
- ByteBuffer directBuffer = nativeAllocateDirectBuffer(size);
- if (directBuffer == null) {
- throw new OutOfMemoryError("allocateDirectBuffer() returned null");
- }
-
- if (!directBuffer.isDirect()) {
- throw new AssertionError("allocateDirectBuffer() did not return a direct buffer");
- }
-
- return directBuffer;
- }
-
- public static ByteBuffer free(ByteBuffer buffer) {
- if (buffer == null) {
- return null;
- }
-
- if (!buffer.isDirect()) {
- throw new IllegalArgumentException("buffer must be direct");
- }
-
- nativeFreeDirectBuffer(buffer);
- return null;
- }
-
- // These JNI methods are implemented in mozglue/android/nsGeckoUtils.cpp.
- private static native ByteBuffer nativeAllocateDirectBuffer(long size);
- private static native void nativeFreeDirectBuffer(ByteBuffer buf);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/GeckoLoader.java
+++ /dev/null
@@ -1,548 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.mozglue;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-import java.text.NumberFormat;
-import java.util.Locale;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
-import android.content.Context;
-import android.os.Build;
-import android.os.Environment;
-import android.util.Log;
-
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.AppConstants;
-
-public final class GeckoLoader {
- private static final String LOGTAG = "GeckoLoader";
-
- private static volatile SafeIntent sIntent;
- private static File sCacheFile;
- private static File sGREDir;
-
- /* Synchronized on GeckoLoader.class. */
- private static boolean sSQLiteLibsLoaded;
- private static boolean sNSSLibsLoaded;
- private static boolean sMozGlueLoaded;
-
- private GeckoLoader() {
- // prevent instantiation
- }
-
- public static File getCacheDir(Context context) {
- if (sCacheFile == null) {
- sCacheFile = context.getCacheDir();
- }
- return sCacheFile;
- }
-
- public static File getGREDir(Context context) {
- if (sGREDir == null) {
- sGREDir = new File(context.getApplicationInfo().dataDir);
- }
- return sGREDir;
- }
-
- private static void setupPluginEnvironment(Context context, String[] pluginDirs) {
- // setup plugin path directories
- try {
- // Check to see if plugins were blocked.
- if (pluginDirs == null) {
- putenv("MOZ_PLUGINS_BLOCKED=1");
- putenv("MOZ_PLUGIN_PATH=");
- return;
- }
-
- StringBuilder pluginSearchPath = new StringBuilder();
- for (int i = 0; i < pluginDirs.length; i++) {
- pluginSearchPath.append(pluginDirs[i]);
- pluginSearchPath.append(":");
- }
- putenv("MOZ_PLUGIN_PATH=" + pluginSearchPath);
-
- File pluginDataDir = context.getDir("plugins", 0);
- putenv("ANDROID_PLUGIN_DATADIR=" + pluginDataDir.getPath());
-
- File pluginPrivateDataDir = context.getDir("plugins_private", 0);
- putenv("ANDROID_PLUGIN_DATADIR_PRIVATE=" + pluginPrivateDataDir.getPath());
-
- } catch (Exception ex) {
- Log.w(LOGTAG, "Caught exception getting plugin dirs.", ex);
- }
- }
-
- private static void setupDownloadEnvironment(final Context context) {
- try {
- File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- File updatesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
- if (downloadDir == null) {
- downloadDir = new File(Environment.getExternalStorageDirectory().getPath(), "download");
- }
- if (updatesDir == null) {
- updatesDir = downloadDir;
- }
- putenv("DOWNLOADS_DIRECTORY=" + downloadDir.getPath());
- putenv("UPDATES_DIRECTORY=" + updatesDir.getPath());
- } catch (Exception e) {
- Log.w(LOGTAG, "No download directory found.", e);
- }
- }
-
- private static void delTree(File file) {
- if (file.isDirectory()) {
- File children[] = file.listFiles();
- for (File child : children) {
- delTree(child);
- }
- }
- file.delete();
- }
-
- private static File getTmpDir(Context context) {
- File tmpDir = context.getDir("tmpdir", Context.MODE_PRIVATE);
- // check if the old tmp dir is there
- File oldDir = new File(tmpDir.getParentFile(), "app_tmp");
- if (oldDir.exists()) {
- delTree(oldDir);
- }
- return tmpDir;
- }
-
- public static void setLastIntent(SafeIntent intent) {
- sIntent = intent;
- }
-
- public static void setupGeckoEnvironment(Context context, String[] pluginDirs, String profilePath) {
- // if we have an intent (we're being launched by an activity)
- // read in any environmental variables from it here
- final SafeIntent intent = sIntent;
- if (intent != null) {
- String env = intent.getStringExtra("env0");
- Log.d(LOGTAG, "Gecko environment env0: " + env);
- for (int c = 1; env != null; c++) {
- putenv(env);
- env = intent.getStringExtra("env" + c);
- Log.d(LOGTAG, "env" + c + ": " + env);
- }
- }
-
- putenv("MOZ_ANDROID_PACKAGE_NAME=" + context.getPackageName());
-
- setupPluginEnvironment(context, pluginDirs);
- setupDownloadEnvironment(context);
-
- // profile home path
- putenv("HOME=" + profilePath);
-
- // setup the tmp path
- File f = getTmpDir(context);
- if (!f.exists()) {
- f.mkdirs();
- }
- putenv("TMPDIR=" + f.getPath());
-
- // setup the downloads path
- f = Environment.getDownloadCacheDirectory();
- putenv("EXTERNAL_STORAGE=" + f.getPath());
-
- // setup the app-specific cache path
- f = context.getCacheDir();
- putenv("CACHE_DIRECTORY=" + f.getPath());
-
- if (AppConstants.Versions.feature17Plus) {
- android.os.UserManager um = (android.os.UserManager)context.getSystemService(Context.USER_SERVICE);
- if (um != null) {
- putenv("MOZ_ANDROID_USER_SERIAL_NUMBER=" + um.getSerialNumberForUser(android.os.Process.myUserHandle()));
- } else {
- Log.d(LOGTAG, "Unable to obtain user manager service on a device with SDK version " + Build.VERSION.SDK_INT);
- }
- }
- setupLocaleEnvironment();
-
- // We don't need this any more.
- sIntent = null;
- }
-
- private static void loadLibsSetupLocked(Context context) {
- // The package data lib directory isn't placed in ld.so's
- // search path, so we have to manually load libraries that
- // libxul will depend on. Not ideal.
-
- File cacheFile = getCacheDir(context);
- putenv("GRE_HOME=" + getGREDir(context).getPath());
-
- // setup the libs cache
- String linkerCache = System.getenv("MOZ_LINKER_CACHE");
- if (linkerCache == null) {
- linkerCache = cacheFile.getPath();
- putenv("MOZ_LINKER_CACHE=" + linkerCache);
- }
-
- // Disable on-demand decompression of the linker on devices where it
- // is known to cause crashes.
- String forced_ondemand = System.getenv("MOZ_LINKER_ONDEMAND");
- if (forced_ondemand == null) {
- if ("HTC".equals(android.os.Build.MANUFACTURER) &&
- "HTC Vision".equals(android.os.Build.MODEL)) {
- putenv("MOZ_LINKER_ONDEMAND=0");
- }
- }
-
- if (AppConstants.MOZ_LINKER_EXTRACT) {
- putenv("MOZ_LINKER_EXTRACT=1");
- // Ensure that the cache dir is world-writable
- File cacheDir = new File(linkerCache);
- if (cacheDir.isDirectory()) {
- cacheDir.setWritable(true, false);
- cacheDir.setExecutable(true, false);
- cacheDir.setReadable(true, false);
- }
- }
- }
-
- @RobocopTarget
- public synchronized static void loadSQLiteLibs(final Context context, final String apkName) {
- if (sSQLiteLibsLoaded) {
- return;
- }
-
- loadMozGlue(context);
- loadLibsSetupLocked(context);
- loadSQLiteLibsNative(apkName);
- sSQLiteLibsLoaded = true;
- }
-
- public synchronized static void loadNSSLibs(final Context context, final String apkName) {
- if (sNSSLibsLoaded) {
- return;
- }
-
- loadMozGlue(context);
- loadLibsSetupLocked(context);
- loadNSSLibsNative(apkName);
- sNSSLibsLoaded = true;
- }
-
- @SuppressWarnings("deprecation")
- private static final String getCPUABI() {
- return android.os.Build.CPU_ABI;
- }
-
- /**
- * Copy a library out of our APK.
- *
- * @param context a Context.
- * @param lib the name of the library; e.g., "mozglue".
- * @param outDir the output directory for the .so. No trailing slash.
- * @return true on success, false on failure.
- */
- private static boolean extractLibrary(final Context context, final String lib, final String outDir) {
- final String apkPath = context.getApplicationInfo().sourceDir;
-
- // Sanity check.
- if (!apkPath.endsWith(".apk")) {
- Log.w(LOGTAG, "sourceDir is not an APK.");
- return false;
- }
-
- // Try to extract the named library from the APK.
- File outDirFile = new File(outDir);
- if (!outDirFile.isDirectory()) {
- if (!outDirFile.mkdirs()) {
- Log.e(LOGTAG, "Couldn't create " + outDir);
- return false;
- }
- }
-
- if (AppConstants.Versions.feature21Plus) {
- String[] abis = Build.SUPPORTED_ABIS;
- for (String abi : abis) {
- if (tryLoadWithABI(lib, outDir, apkPath, abi)) {
- return true;
- }
- }
- return false;
- } else {
- final String abi = getCPUABI();
- return tryLoadWithABI(lib, outDir, apkPath, abi);
- }
- }
-
- private static boolean tryLoadWithABI(String lib, String outDir, String apkPath, String abi) {
- try {
- final ZipFile zipFile = new ZipFile(new File(apkPath));
- try {
- final String libPath = "lib/" + abi + "/lib" + lib + ".so";
- final ZipEntry entry = zipFile.getEntry(libPath);
- if (entry == null) {
- Log.w(LOGTAG, libPath + " not found in APK " + apkPath);
- return false;
- }
-
- final InputStream in = zipFile.getInputStream(entry);
- try {
- final String outPath = outDir + "/lib" + lib + ".so";
- final FileOutputStream out = new FileOutputStream(outPath);
- final byte[] bytes = new byte[1024];
- int read;
-
- Log.d(LOGTAG, "Copying " + libPath + " to " + outPath);
- boolean failed = false;
- try {
- while ((read = in.read(bytes, 0, 1024)) != -1) {
- out.write(bytes, 0, read);
- }
- } catch (Exception e) {
- Log.w(LOGTAG, "Failing library copy.", e);
- failed = true;
- } finally {
- out.close();
- }
-
- if (failed) {
- // Delete the partial copy so we don't fail to load it.
- // Don't bother to check the return value -- there's nothing
- // we can do about a failure.
- new File(outPath).delete();
- } else {
- // Mark the file as executable. This doesn't seem to be
- // necessary for the loader, but it's the normal state of
- // affairs.
- Log.d(LOGTAG, "Marking " + outPath + " as executable.");
- new File(outPath).setExecutable(true);
- }
-
- return !failed;
- } finally {
- in.close();
- }
- } finally {
- zipFile.close();
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to extract lib from APK.", e);
- return false;
- }
- }
-
- private static String getLoadDiagnostics(final Context context, final String lib) {
- final String androidPackageName = context.getPackageName();
-
- final StringBuilder message = new StringBuilder("LOAD ");
- message.append(lib);
-
- // These might differ. If so, we know why the library won't load!
- message.append(": ABI: " + AppConstants.MOZ_APP_ABI + ", " + getCPUABI());
- message.append(": Data: " + context.getApplicationInfo().dataDir);
- try {
- final boolean appLibExists = new File("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so").exists();
- final boolean dataDataExists = new File("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so").exists();
- message.append(", ax=" + appLibExists);
- message.append(", ddx=" + dataDataExists);
- } catch (Throwable e) {
- message.append(": ax/ddx fail, ");
- }
-
- try {
- final String dashOne = "/data/data/" + androidPackageName + "-1";
- final String dashTwo = "/data/data/" + androidPackageName + "-2";
- final boolean dashOneExists = new File(dashOne).exists();
- final boolean dashTwoExists = new File(dashTwo).exists();
- message.append(", -1x=" + dashOneExists);
- message.append(", -2x=" + dashTwoExists);
- } catch (Throwable e) {
- message.append(", dash fail, ");
- }
-
- try {
- if (Build.VERSION.SDK_INT >= 9) {
- final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
- final boolean nativeLibDirExists = new File(nativeLibPath).exists();
- final boolean nativeLibLibExists = new File(nativeLibPath + "/lib" + lib + ".so").exists();
-
- message.append(", nativeLib: " + nativeLibPath);
- message.append(", dirx=" + nativeLibDirExists);
- message.append(", libx=" + nativeLibLibExists);
- } else {
- message.append(", <pre-9>");
- }
- } catch (Throwable e) {
- message.append(", nativeLib fail.");
- }
-
- return message.toString();
- }
-
- private static final boolean attemptLoad(final String path) {
- try {
- System.load(path);
- return true;
- } catch (Throwable e) {
- Log.wtf(LOGTAG, "Couldn't load " + path + ": " + e);
- }
-
- return false;
- }
-
- /**
- * The first two attempts at loading a library: directly, and
- * then using the app library path.
- *
- * Returns null or the cause exception.
- */
- private static final Throwable doLoadLibraryExpected(final Context context, final String lib) {
- try {
- // Attempt 1: the way that should work.
- System.loadLibrary(lib);
- return null;
- } catch (Throwable e) {
- Log.wtf(LOGTAG, "Couldn't load " + lib + ". Trying native library dir.");
-
- if (Build.VERSION.SDK_INT < 9) {
- // We can't use nativeLibraryDir.
- return e;
- }
-
- // Attempt 2: use nativeLibraryDir, which should also work.
- final String libDir = context.getApplicationInfo().nativeLibraryDir;
- final String libPath = libDir + "/lib" + lib + ".so";
-
- // Does it even exist?
- if (new File(libPath).exists()) {
- if (attemptLoad(libPath)) {
- // Success!
- return null;
- }
- Log.wtf(LOGTAG, "Library exists but couldn't load!");
- } else {
- Log.wtf(LOGTAG, "Library doesn't exist when it should.");
- }
-
- // We failed. Return the original cause.
- return e;
- }
- }
-
- public static void doLoadLibrary(final Context context, final String lib) {
- final Throwable e = doLoadLibraryExpected(context, lib);
- if (e == null) {
- // Success.
- return;
- }
-
- // If we're in a mismatched UID state (Bug 1042935 Comment 16) there's really
- // nothing we can do.
- if (Build.VERSION.SDK_INT >= 9) {
- final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
- if (nativeLibPath.contains("mismatched_uid")) {
- throw new RuntimeException("Fatal: mismatched UID: cannot load.");
- }
- }
-
- // Attempt 3: try finding the path the pseudo-supported way using .dataDir.
- final String dataLibPath = context.getApplicationInfo().dataDir + "/lib/lib" + lib + ".so";
- if (attemptLoad(dataLibPath)) {
- return;
- }
-
- // Attempt 4: use /data/app-lib directly. This is a last-ditch effort.
- final String androidPackageName = context.getPackageName();
- if (attemptLoad("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so")) {
- return;
- }
-
- // Attempt 5: even more optimistic.
- if (attemptLoad("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so")) {
- return;
- }
-
- // Look in our files directory, copying from the APK first if necessary.
- final String filesLibDir = context.getFilesDir() + "/lib";
- final String filesLibPath = filesLibDir + "/lib" + lib + ".so";
- if (new File(filesLibPath).exists()) {
- if (attemptLoad(filesLibPath)) {
- return;
- }
- } else {
- // Try copying.
- if (extractLibrary(context, lib, filesLibDir)) {
- // Let's try it!
- if (attemptLoad(filesLibPath)) {
- return;
- }
- }
- }
-
- // Give up loudly, leaking information to debug the failure.
- final String message = getLoadDiagnostics(context, lib);
- Log.e(LOGTAG, "Load diagnostics: " + message);
-
- // Throw the descriptive message, using the original library load
- // failure as the cause.
- throw new RuntimeException(message, e);
- }
-
- public synchronized static void loadMozGlue(final Context context) {
- if (sMozGlueLoaded) {
- return;
- }
-
- doLoadLibrary(context, "mozglue");
- sMozGlueLoaded = true;
- }
-
- public synchronized static void loadGeckoLibs(final Context context, final String apkName) {
- loadLibsSetupLocked(context);
- loadGeckoLibsNative(apkName);
- }
-
- private static void setupLocaleEnvironment() {
- putenv("LANG=" + Locale.getDefault().toString());
- NumberFormat nf = NumberFormat.getInstance();
- if (nf instanceof DecimalFormat) {
- DecimalFormat df = (DecimalFormat)nf;
- DecimalFormatSymbols dfs = df.getDecimalFormatSymbols();
-
- putenv("LOCALE_DECIMAL_POINT=" + dfs.getDecimalSeparator());
- putenv("LOCALE_THOUSANDS_SEP=" + dfs.getGroupingSeparator());
- putenv("LOCALE_GROUPING=" + (char)df.getGroupingSize());
- }
- }
-
- @SuppressWarnings("serial")
- public static class AbortException extends Exception {
- public AbortException(String msg) {
- super(msg);
- }
- }
-
- @JNITarget
- public static void abort(final String msg) {
- final Thread thread = Thread.currentThread();
- final Thread.UncaughtExceptionHandler uncaughtHandler =
- thread.getUncaughtExceptionHandler();
- if (uncaughtHandler != null) {
- uncaughtHandler.uncaughtException(thread, new AbortException(msg));
- }
- }
-
- // These methods are implemented in mozglue/android/nsGeckoUtils.cpp
- private static native void putenv(String map);
-
- // These methods are implemented in mozglue/android/APKOpen.cpp
- public static native void nativeRun(String args);
- private static native void loadGeckoLibsNative(String apkName);
- private static native void loadSQLiteLibsNative(String apkName);
- private static native void loadNSSLibsNative(String apkName);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/JNIObject.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.mozilla.gecko.mozglue;
-
-// Class that all classes with native methods extend from.
-public abstract class JNIObject
-{
- // Pointer to a WeakPtr object that refers to the native object.
- private long mHandle;
-
- // Dispose of any reference to a native object.
- protected abstract void disposeNative();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/NativeReference.java
+++ /dev/null
@@ -1,13 +0,0 @@
-/* -*- 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.mozglue;
-
-public interface NativeReference
-{
- public void release();
-
- public boolean isReleased();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/NativeZip.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/* -*- 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.mozglue;
-
-import org.mozilla.gecko.annotation.JNITarget;
-
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.util.zip.Inflater;
-import java.util.zip.InflaterInputStream;
-
-public class NativeZip implements NativeReference {
- private static final int DEFLATE = 8;
- private static final int STORE = 0;
-
- private volatile long mObj;
- private InputStream mInput;
-
- public NativeZip(String path) {
- mObj = getZip(path);
- }
-
- public NativeZip(InputStream input) {
- if (!(input instanceof ByteBufferInputStream)) {
- throw new IllegalArgumentException("Got " + input.getClass()
- + ", but expected ByteBufferInputStream!");
- }
- ByteBufferInputStream bbinput = (ByteBufferInputStream)input;
- mObj = getZipFromByteBuffer(bbinput.mBuf);
- mInput = input;
- }
-
- @Override
- public void finalize() {
- release();
- }
-
- public void close() {
- release();
- }
-
- @Override
- public void release() {
- if (mObj != 0) {
- _release(mObj);
- mObj = 0;
- }
- mInput = null;
- }
-
- @Override
- public boolean isReleased() {
- return (mObj == 0);
- }
-
- public InputStream getInputStream(String path) {
- if (isReleased()) {
- throw new IllegalStateException("Can't get path \"" + path
- + "\" because NativeZip is closed!");
- }
- return _getInputStream(mObj, path);
- }
-
- private static native long getZip(String path);
- private static native long getZipFromByteBuffer(ByteBuffer buffer);
- private static native void _release(long obj);
- private native InputStream _getInputStream(long obj, String path);
-
- @JNITarget
- private InputStream createInputStream(ByteBuffer buffer, int compression) {
- if (compression != STORE && compression != DEFLATE) {
- throw new IllegalArgumentException("Unexpected compression: " + compression);
- }
-
- InputStream input = new ByteBufferInputStream(buffer, this);
- if (compression == DEFLATE) {
- Inflater inflater = new Inflater(true);
- input = new InflaterInputStream(input, inflater);
- }
-
- return input;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/SafeIntent.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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/.
- */
-
-// This should be in util/, but is here because of build dependency issues.
-package org.mozilla.gecko.mozglue;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.util.Log;
-
-/**
- * External applications can pass values into Intents that can cause us to crash: in defense,
- * we wrap {@link Intent} and catch the exceptions they may force us to throw. See bug 1090385
- * for more.
- */
-public class SafeIntent {
- private static final String LOGTAG = "Gecko" + SafeIntent.class.getSimpleName();
-
- private final Intent intent;
-
- public SafeIntent(final Intent intent) {
- this.intent = intent;
- }
-
- public boolean getBooleanExtra(final String name, final boolean defaultValue) {
- try {
- return intent.getBooleanExtra(name, defaultValue);
- } catch (OutOfMemoryError e) {
- Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
- return defaultValue;
- } catch (RuntimeException e) {
- Log.w(LOGTAG, "Couldn't get intent extras.", e);
- return defaultValue;
- }
- }
-
- public String getStringExtra(final String name) {
- try {
- return intent.getStringExtra(name);
- } catch (OutOfMemoryError e) {
- Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
- return null;
- } catch (RuntimeException e) {
- Log.w(LOGTAG, "Couldn't get intent extras.", e);
- return null;
- }
- }
-
- public Bundle getBundleExtra(final String name) {
- try {
- return intent.getBundleExtra(name);
- } catch (OutOfMemoryError e) {
- Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
- return null;
- } catch (RuntimeException e) {
- Log.w(LOGTAG, "Couldn't get intent extras.", e);
- return null;
- }
- }
-
- public String getAction() {
- return intent.getAction();
- }
-
- public String getDataString() {
- try {
- return intent.getDataString();
- } catch (OutOfMemoryError e) {
- Log.w(LOGTAG, "Couldn't get intent data string: OOM. Malformed?");
- return null;
- } catch (RuntimeException e) {
- Log.w(LOGTAG, "Couldn't get intent data string.", e);
- return null;
- }
- }
-
- public Uri getData() {
- try {
- return intent.getData();
- } catch (OutOfMemoryError e) {
- Log.w(LOGTAG, "Couldn't get intent data: OOM. Malformed?");
- return null;
- } catch (RuntimeException e) {
- Log.w(LOGTAG, "Couldn't get intent data.", e);
- return null;
- }
- }
-
- public Intent getUnsafe() {
- return intent;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/permissions/PermissionBlock.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* -*- 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.permissions;
-
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.Activity;
-import android.content.Context;
-import android.support.annotation.NonNull;
-
-/**
- * Helper class to run code blocks depending on whether a user has granted or denied certain runtime permissions.
- */
-public class PermissionBlock {
- private final PermissionsHelper helper;
-
- private Context context;
- private String[] permissions;
- private boolean onUIThread;
- private Runnable onPermissionsGranted;
- private Runnable onPermissionsDenied;
- private boolean doNotPrompt;
-
- /* package-private */ PermissionBlock(Context context, PermissionsHelper helper) {
- this.context = context;
- this.helper = helper;
- }
-
- /**
- * Determine whether the app has been granted the specified permissions.
- */
- public PermissionBlock withPermissions(@NonNull String... permissions) {
- this.permissions = permissions;
- return this;
- }
-
- /**
- * Execute all callbacks on the UI thread.
- */
- public PermissionBlock onUIThread() {
- this.onUIThread = true;
- return this;
- }
-
- /**
- * Do not prompt the user to accept the permission if it has not been granted yet.
- */
- public PermissionBlock doNotPrompt() {
- doNotPrompt = true;
- return this;
- }
-
- /**
- * If the condition is true then do not prompt the user to accept the permission if it has not
- * been granted yet.
- */
- public PermissionBlock doNotPromptIf(boolean condition) {
- if (condition) {
- doNotPrompt();
- }
-
- return this;
- }
-
- /**
- * Execute this permission block. Calling this method will prompt the user if needed.
- */
- public void run() {
- run(null);
- }
-
- /**
- * Execute the specified runnable if the app has been granted all permissions. Calling this method will prompt the
- * user if needed.
- */
- public void run(Runnable onPermissionsGranted) {
- if (!doNotPrompt && !(context instanceof Activity)) {
- throw new IllegalStateException("You need to either specify doNotPrompt() or pass in an Activity context");
- }
-
- this.onPermissionsGranted = onPermissionsGranted;
-
- if (hasPermissions(context)) {
- onPermissionsGranted();
- } else if (doNotPrompt) {
- onPermissionsDenied();
- } else {
- Permissions.prompt((Activity) context, this);
- }
-
- // This reference is no longer needed. Let's clear it now to avoid memory leaks.
- context = null;
- }
-
- /**
- * Execute this fallback if at least one permission has not been granted.
- */
- public PermissionBlock andFallback(@NonNull Runnable onPermissionsDenied) {
- this.onPermissionsDenied = onPermissionsDenied;
- return this;
- }
-
- /* package-private */ void onPermissionsGranted() {
- executeRunnable(onPermissionsGranted);
- }
-
- /* package-private */ void onPermissionsDenied() {
- executeRunnable(onPermissionsDenied);
- }
-
- private void executeRunnable(Runnable runnable) {
- if (runnable == null) {
- return;
- }
-
- if (onUIThread) {
- ThreadUtils.postToUiThread(runnable);
- } else {
- runnable.run();
- }
- }
-
- /* package-private */ String[] getPermissions() {
- return permissions;
- }
-
- /* packacge-private */ boolean hasPermissions(Context context) {
- return helper.hasPermissions(context, permissions);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/permissions/Permissions.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/* -*- 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.permissions;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.support.annotation.NonNull;
-
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
-
-/**
- * Convenience class for checking and prompting for runtime permissions.
- *
- * Example:
- *
- * Permissions.from(activity)
- * .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- * .onUiThread()
- * .andFallback(onPermissionDenied())
- * .run(onPermissionGranted())
- *
- * This example will run the runnable returned by onPermissionGranted() if the WRITE_EXTERNAL_STORAGE permission is
- * already granted. Otherwise it will prompt the user and run the runnable returned by onPermissionGranted() or
- * onPermissionDenied() depending on whether the user accepted or not. If onUiThread() is specified then all callbacks
- * will be run on the UI thread.
- */
-public class Permissions {
- private static final Queue<PermissionBlock> waiting = new LinkedList<>();
- private static final Queue<PermissionBlock> prompt = new LinkedList<>();
-
- private static PermissionsHelper permissionHelper = new PermissionsHelper();
-
- /**
- * Entry point for checking (and optionally prompting for) runtime permissions.
- *
- * Note: The provided context needs to be an Activity context in order to prompt. Use doNotPrompt()
- * for all other contexts.
- */
- public static PermissionBlock from(@NonNull Context context) {
- return new PermissionBlock(context, permissionHelper);
- }
-
- /**
- * This method will block until the specified permissions have been granted or denied by the user.
- * If needed the user will be prompted.
- *
- * @return true if all of the permissions have been granted. False if any of the permissions have been denied.
- */
- public static boolean waitFor(@NonNull Activity activity, String... permissions) {
- ThreadUtils.assertNotOnUiThread(); // We do not want to block the UI thread.
-
- // This task will block until all of the permissions have been granted
- final FutureTask<Boolean> blockingTask = new FutureTask<>(new Callable<Boolean>() {
- @Override
- public Boolean call() throws Exception {
- return true;
- }
- });
-
- // This runnable will cancel the task if any of the permissions have been denied
- Runnable cancelBlockingTask = new Runnable() {
- @Override
- public void run() {
- blockingTask.cancel(true);
- }
- };
-
- Permissions.from(activity)
- .withPermissions(permissions)
- .andFallback(cancelBlockingTask)
- .run(blockingTask);
-
- try {
- return blockingTask.get();
- } catch (InterruptedException | ExecutionException | CancellationException e) {
- return false;
- }
- }
-
- /**
- * Determine whether you have been granted particular permissions.
- */
- public static boolean has(Context context, String... permissions) {
- return permissionHelper.hasPermissions(context, permissions);
- }
-
- /* package-private */ static void setPermissionHelper(PermissionsHelper permissionHelper) {
- Permissions.permissionHelper = permissionHelper;
- }
-
- /**
- * Callback for Activity.onRequestPermissionsResult(). All activities that prompt for permissions using this class
- * should implement onRequestPermissionsResult() and call this method.
- */
- public static synchronized void onRequestPermissionsResult(@NonNull Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults) {
- processGrantResults(permissions, grantResults);
-
- processQueue(activity, permissions, grantResults);
- }
-
- /* package-private */ static synchronized void prompt(Activity activity, PermissionBlock block) {
- if (prompt.isEmpty()) {
- prompt.add(block);
- showPrompt(activity);
- } else {
- waiting.add(block);
- }
- }
-
- private static synchronized void processGrantResults(@NonNull String[] permissions, @NonNull int[] grantResults) {
- final HashSet<String> grantedPermissions = collectGrantedPermissions(permissions, grantResults);
-
- while (!prompt.isEmpty()) {
- final PermissionBlock block = prompt.poll();
-
- if (allPermissionsGranted(block, grantedPermissions)) {
- block.onPermissionsGranted();
- } else {
- block.onPermissionsDenied();
- }
- }
- }
-
- private static synchronized void processQueue(Activity activity, String[] permissions, int[] grantResults) {
- final HashSet<String> deniedPermissions = collectDeniedPermissions(permissions, grantResults);
-
- while (!waiting.isEmpty()) {
- final PermissionBlock block = waiting.poll();
-
- if (block.hasPermissions(activity)) {
- block.onPermissionsGranted();
- } else {
- if (atLeastOnePermissionDenied(block, deniedPermissions)) {
- // We just prompted the user and one of the permissions of this block has been denied:
- // There's no reason to instantly prompt again; Just reject without prompting.
- block.onPermissionsDenied();
- } else {
- prompt.add(block);
- }
- }
- }
-
- if (!prompt.isEmpty()) {
- showPrompt(activity);
- }
- }
-
- private static synchronized void showPrompt(Activity activity) {
- HashSet<String> permissions = new HashSet<>();
-
- for (PermissionBlock block : prompt) {
- Collections.addAll(permissions, block.getPermissions());
- }
-
- permissionHelper.prompt(activity, permissions.toArray(new String[permissions.size()]));
- }
-
- private static HashSet<String> collectGrantedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
- return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_GRANTED);
- }
-
- private static HashSet<String> collectDeniedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
- return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_DENIED);
- }
-
- private static HashSet<String> filterPermissionsByResult(@NonNull String[] permissions, @NonNull int[] grantResults, int result) {
- HashSet<String> grantedPermissions = new HashSet<>(permissions.length);
- for (int i = 0; i < permissions.length; i++) {
- if (grantResults[i] == result) {
- grantedPermissions.add(permissions[i]);
- }
- }
- return grantedPermissions;
- }
-
- private static boolean allPermissionsGranted(PermissionBlock block, HashSet<String> grantedPermissions) {
- for (String permission : block.getPermissions()) {
- if (!grantedPermissions.contains(permission)) {
- return false;
- }
- }
-
- return true;
- }
-
- private static boolean atLeastOnePermissionDenied(PermissionBlock block, HashSet<String> deniedPermissions) {
- for (String permission : block.getPermissions()) {
- if (deniedPermissions.contains(permission)) {
- return true;
- }
- }
-
- return false;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/permissions/PermissionsHelper.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/* -*- 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.permissions;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.content.ContextCompat;
-
-/* package-private */ class PermissionsHelper {
- private static final int PERMISSIONS_REQUEST_CODE = 212;
-
- public boolean hasPermissions(Context context, String... permissions) {
- for (String permission : permissions) {
- final int permissionCheck = ContextCompat.checkSelfPermission(context, permission);
-
- if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
- return false;
- }
- }
-
- return true;
- }
-
- public void prompt(Activity activity, String[] permissions) {
- ActivityCompat.requestPermissions(activity, permissions, PERMISSIONS_REQUEST_CODE);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/* -*- 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.restrictions;
-
-/**
- * Default implementation of RestrictionConfiguration interface. Used whenever no restrictions are enforced for the
- * current profile.
- */
-public class DefaultConfiguration implements RestrictionConfiguration {
- @Override
- public boolean isAllowed(Restrictable restrictable) {
- if (restrictable == Restrictable.BLOCK_LIST) {
- return false;
- }
-
- return true;
- }
-
- @Override
- public boolean canLoadUrl(String url) {
- return true;
- }
-
- @Override
- public boolean isRestricted() {
- return false;
- }
-
- @Override
- public void update() {}
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/* -*- 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.restrictions;
-
-import android.net.Uri;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * RestrictionConfiguration implementation for guest profiles.
- */
-public class GuestProfileConfiguration implements RestrictionConfiguration {
- static List<Restrictable> DISABLED_FEATURES = Arrays.asList(
- Restrictable.DOWNLOAD,
- Restrictable.INSTALL_EXTENSION,
- Restrictable.INSTALL_APPS,
- Restrictable.BROWSE,
- Restrictable.SHARE,
- Restrictable.BOOKMARK,
- Restrictable.ADD_CONTACT,
- Restrictable.SET_IMAGE,
- Restrictable.MODIFY_ACCOUNTS,
- Restrictable.REMOTE_DEBUGGING,
- Restrictable.IMPORT_SETTINGS,
- Restrictable.BLOCK_LIST,
- Restrictable.DATA_CHOICES,
- Restrictable.DEFAULT_THEME
- );
-
- @SuppressWarnings("serial")
- private static final List<String> BANNED_SCHEMES = Arrays.asList(
- "file",
- "chrome",
- "resource",
- "jar",
- "wyciwyg"
- );
-
- private static final List<String> BANNED_URLS = Arrays.asList(
- "about:config",
- "about:addons"
- );
-
- @Override
- public boolean isAllowed(Restrictable restrictable) {
- return !DISABLED_FEATURES.contains(restrictable);
- }
-
- @Override
- public boolean canLoadUrl(String url) {
- // Null URLs are always permitted.
- if (url == null) {
- return true;
- }
-
- final Uri u = Uri.parse(url);
- final String scheme = u.getScheme();
- if (BANNED_SCHEMES.contains(scheme)) {
- return false;
- }
-
- url = url.toLowerCase();
- for (String banned : BANNED_URLS) {
- if (url.startsWith(banned)) {
- return false;
- }
- }
-
- return true;
- }
-
- @Override
- public boolean isRestricted() {
- return true;
- }
-
- @Override
- public void update() {}
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/* -*- 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.restrictions;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.support.annotation.StringRes;
-
-/**
- * This is a list of things we can restrict you from doing. Some of these are reflected in Android UserManager constants.
- * Others are specific to us.
- * These constants should be in sync with the ones from toolkit/components/parentalcontrols/nsIParentalControlsService.idl
- */
-public enum Restrictable {
- DOWNLOAD(1, "downloads", 0, 0),
-
- INSTALL_EXTENSION(
- 2, "no_install_extensions",
- R.string.restrictable_feature_addons_installation,
- R.string.restrictable_feature_addons_installation_description),
-
- // UserManager.DISALLOW_INSTALL_APPS
- INSTALL_APPS(3, "no_install_apps", 0 , 0),
-
- BROWSE(4, "browse", 0, 0),
-
- SHARE(5, "share", 0, 0),
-
- BOOKMARK(6, "bookmark", 0, 0),
-
- ADD_CONTACT(7, "add_contact", 0, 0),
-
- SET_IMAGE(8, "set_image", 0, 0),
-
- // UserManager.DISALLOW_MODIFY_ACCOUNTS
- MODIFY_ACCOUNTS(9, "no_modify_accounts", 0, 0),
-
- REMOTE_DEBUGGING(10, "remote_debugging", 0, 0),
-
- IMPORT_SETTINGS(11, "import_settings", 0, 0),
-
- PRIVATE_BROWSING(
- 12, "private_browsing",
- R.string.restrictable_feature_private_browsing,
- R.string.restrictable_feature_private_browsing_description),
-
- DATA_CHOICES(13, "data_coices", 0, 0),
-
- CLEAR_HISTORY(14, "clear_history",
- R.string.restrictable_feature_clear_history,
- R.string.restrictable_feature_clear_history_description),
-
- MASTER_PASSWORD(15, "master_password", 0, 0),
-
- GUEST_BROWSING(16, "guest_browsing", 0, 0),
-
- ADVANCED_SETTINGS(17, "advanced_settings",
- R.string.restrictable_feature_advanced_settings,
- R.string.restrictable_feature_advanced_settings_description),
-
- CAMERA_MICROPHONE(18, "camera_microphone",
- R.string.restrictable_feature_camera_microphone,
- R.string.restrictable_feature_camera_microphone_description),
-
- BLOCK_LIST(19, "block_list",
- R.string.restrictable_feature_block_list,
- R.string.restrictable_feature_block_list_description),
-
- TELEMETRY(20, "telemetry",
- R.string.datareporting_telemetry_title,
- R.string.datareporting_telemetry_summary),
-
- HEALTH_REPORT(21, "health_report",
- R.string.datareporting_fhr_title,
- R.string.datareporting_fhr_summary2),
-
- DEFAULT_THEME(22, "default_theme", 0, 0);
-
- public final int id;
- public final String name;
-
- @StringRes
- public final int title;
-
- @StringRes
- public final int description;
-
- Restrictable(final int id, final String name, @StringRes int title, @StringRes int description) {
- this.id = id;
- this.name = name;
- this.title = title;
- this.description = description;
- }
-
- public String getTitle(Context context) {
- if (title == 0) {
- return toString();
- }
- return context.getResources().getString(title);
- }
-
- public String getDescription(Context context) {
- if (description == 0) {
- return null;
- }
- return context.getResources().getString(description);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/* -*- 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.restrictions;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.StrictMode;
-import android.os.UserManager;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-public class RestrictedProfileConfiguration implements RestrictionConfiguration {
- // Mapping from restrictable feature to default state (on/off)
- private static Map<Restrictable, Boolean> configuration = new LinkedHashMap<>();
- static {
- configuration.put(Restrictable.INSTALL_EXTENSION, false);
- configuration.put(Restrictable.PRIVATE_BROWSING, false);
- configuration.put(Restrictable.CLEAR_HISTORY, false);
- configuration.put(Restrictable.MASTER_PASSWORD, false);
- configuration.put(Restrictable.GUEST_BROWSING, false);
- configuration.put(Restrictable.ADVANCED_SETTINGS, false);
- configuration.put(Restrictable.CAMERA_MICROPHONE, false);
- configuration.put(Restrictable.DATA_CHOICES, false);
- configuration.put(Restrictable.BLOCK_LIST, false);
- configuration.put(Restrictable.TELEMETRY, false);
- configuration.put(Restrictable.HEALTH_REPORT, true);
- configuration.put(Restrictable.DEFAULT_THEME, true);
- }
-
- /**
- * These restrictions are hidden from the admin configuration UI.
- */
- private static List<Restrictable> hiddenRestrictions = new ArrayList<>();
- static {
- hiddenRestrictions.add(Restrictable.MASTER_PASSWORD);
- hiddenRestrictions.add(Restrictable.GUEST_BROWSING);
- hiddenRestrictions.add(Restrictable.DATA_CHOICES);
- hiddenRestrictions.add(Restrictable.DEFAULT_THEME);
-
- // Hold behind Nightly flag until we have an actual block list deployed.
- if (!AppConstants.NIGHTLY_BUILD) {
- hiddenRestrictions.add(Restrictable.BLOCK_LIST);
- }
- }
-
- /* package-private */ static boolean shouldHide(Restrictable restrictable) {
- return hiddenRestrictions.contains(restrictable);
- }
-
- /* package-private */ static Map<Restrictable, Boolean> getConfiguration() {
- return configuration;
- }
-
- private Context context;
-
- public RestrictedProfileConfiguration(Context context) {
- this.context = context.getApplicationContext();
- }
-
- @Override
- public synchronized boolean isAllowed(Restrictable restrictable) {
- // Special casing system/user restrictions
- if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) {
- return RestrictionCache.getUserRestriction(context, restrictable.name);
- }
-
- if (!RestrictionCache.hasApplicationRestriction(context, restrictable.name) && !configuration.containsKey(restrictable)) {
- // Always allow features that are not in the configuration
- return true;
- }
-
- return RestrictionCache.getApplicationRestriction(context, restrictable.name, configuration.get(restrictable));
- }
-
- @Override
- public boolean canLoadUrl(String url) {
- if (!isAllowed(Restrictable.INSTALL_EXTENSION) && AboutPages.isAboutAddons(url)) {
- return false;
- }
-
- if (!isAllowed(Restrictable.PRIVATE_BROWSING) && AboutPages.isAboutPrivateBrowsing(url)) {
- return false;
- }
-
- if (AboutPages.isAboutConfig(url)) {
- // Always block access to about:config to prevent circumventing restrictions (Bug 1189233)
- return false;
- }
-
- return true;
- }
-
- @Override
- public boolean isRestricted() {
- return true;
- }
-
- @Override
- public synchronized void update() {
- RestrictionCache.invalidate();
- }
-
- public static List<Restrictable> getVisibleRestrictions() {
- final List<Restrictable> visibleList = new ArrayList<>();
-
- for (Restrictable restrictable : configuration.keySet()) {
- if (hiddenRestrictions.contains(restrictable)) {
- continue;
- }
- visibleList.add(restrictable);
- }
-
- return visibleList;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/* -*- 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.restrictions;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.StrictMode;
-import android.os.UserManager;
-
-import org.mozilla.gecko.util.ThreadUtils;
-
-/**
- * Cache for user and application restrictions.
- */
-public class RestrictionCache {
- private static Bundle cachedAppRestrictions;
- private static Bundle cachedUserRestrictions;
- private static boolean isCacheInvalid = true;
-
- private RestrictionCache() {}
-
- public static synchronized boolean getUserRestriction(Context context, String restriction) {
- updateCacheIfNeeded(context);
- return cachedUserRestrictions.getBoolean(restriction);
- }
-
- public static synchronized boolean hasApplicationRestriction(Context context, String restriction) {
- updateCacheIfNeeded(context);
- return cachedAppRestrictions.containsKey(restriction);
- }
-
- public static synchronized boolean getApplicationRestriction(Context context, String restriction, boolean defaultValue) {
- updateCacheIfNeeded(context);
- return cachedAppRestrictions.getBoolean(restriction, defaultValue);
- }
-
- public static synchronized boolean hasApplicationRestrictions(Context context) {
- updateCacheIfNeeded(context);
- return !cachedAppRestrictions.isEmpty();
- }
-
- public static synchronized void invalidate() {
- isCacheInvalid = true;
- }
-
- private static void updateCacheIfNeeded(Context context) {
- // If we are not on the UI thread then we can just go ahead and read the values (Bug 1189347).
- // Otherwise we read from the cache to avoid blocking the UI thread. If the cache is invalid
- // then we hazard the consequences and just do the read.
- if (isCacheInvalid || !ThreadUtils.isOnUiThread()) {
- readRestrictions(context);
- isCacheInvalid = false;
- }
- }
-
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
- private static void readRestrictions(Context context) {
- final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE);
-
- // If we do not have anything in the cache yet then this read might happen on the UI thread (Bug 1189347).
- final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
-
- try {
- Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName());
- migrateRestrictionsIfNeeded(appRestrictions);
-
- cachedAppRestrictions = appRestrictions;
- cachedUserRestrictions = mgr.getUserRestrictions(); // Always implies disk read
- } finally {
- StrictMode.setThreadPolicy(policy);
- }
- }
-
- /**
- * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336).
- */
- /* package-private */ static void migrateRestrictionsIfNeeded(Bundle bundle) {
- if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) {
- bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions"));
- }
-
- if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) {
- bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing"));
- }
-
- if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) {
- bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history"));
- }
-
- if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) {
- bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings"));
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/* -*- 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.restrictions;
-
-/**
- * Interface for classes that Restrictions will delegate to for making decisions.
- */
-public interface RestrictionConfiguration {
- /**
- * Is the user allowed to perform this action?
- */
- boolean isAllowed(Restrictable restrictable);
-
- /**
- * Is the user allowed to load the given URL?
- */
- boolean canLoadUrl(String url);
-
- /**
- * Is this user restricted in any way?
- */
- boolean isRestricted();
-
- /**
- * Update restrictions if needed.
- */
- void update();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/* -*- 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.restrictions;
-
-import org.mozilla.gecko.AppConstants;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.RestrictionEntry;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.Map;
-
-/**
- * Broadcast receiver providing supported restrictions to the system.
- */
-@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-public class RestrictionProvider extends BroadcastReceiver {
- @Override
- public void onReceive(final Context context, final Intent intent) {
- if (AppConstants.Versions.preJBMR2) {
- // This broadcast does not make any sense prior to Jelly Bean MR2.
- return;
- }
-
- final PendingResult result = goAsync();
-
- new Thread() {
- @Override
- public void run() {
- final Bundle oldRestrictions = intent.getBundleExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE);
- RestrictionCache.migrateRestrictionsIfNeeded(oldRestrictions);
-
- final Bundle extras = new Bundle();
-
- ArrayList<RestrictionEntry> entries = initRestrictions(context, oldRestrictions);
- extras.putParcelableArrayList(Intent.EXTRA_RESTRICTIONS_LIST, entries);
-
- result.setResult(Activity.RESULT_OK, null, extras);
- result.finish();
- }
- }.start();
- }
-
- private ArrayList<RestrictionEntry> initRestrictions(Context context, Bundle oldRestrictions) {
- ArrayList<RestrictionEntry> entries = new ArrayList<RestrictionEntry>();
-
- final Map<Restrictable, Boolean> configuration = RestrictedProfileConfiguration.getConfiguration();
-
- for (Restrictable restrictable : configuration.keySet()) {
- if (RestrictedProfileConfiguration.shouldHide(restrictable)) {
- continue;
- }
-
- RestrictionEntry entry = createRestrictionEntryWithDefaultValue(context, restrictable,
- oldRestrictions.getBoolean(restrictable.name, configuration.get(restrictable)));
- entries.add(entry);
- }
-
- return entries;
- }
-
- private RestrictionEntry createRestrictionEntryWithDefaultValue(Context context, Restrictable restrictable, boolean defaultValue) {
- RestrictionEntry entry = new RestrictionEntry(restrictable.name, defaultValue);
-
- entry.setTitle(restrictable.getTitle(context));
-
- final String description = restrictable.getDescription(context);
- if (!TextUtils.isEmpty(description)) {
- entry.setDescription(description);
- }
-
- return entry;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* -*- 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.restrictions;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.restrictions.DefaultConfiguration;
-import org.mozilla.gecko.restrictions.GuestProfileConfiguration;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
-import org.mozilla.gecko.restrictions.RestrictionCache;
-import org.mozilla.gecko.restrictions.RestrictionConfiguration;
-
-@RobocopTarget
-public class Restrictions {
- private static final String LOGTAG = "GeckoRestrictedProfiles";
-
- private static RestrictionConfiguration configuration;
-
- private static RestrictionConfiguration getConfiguration(Context context) {
- if (configuration == null) {
- configuration = createConfiguration(context);
- }
-
- return configuration;
- }
-
- public static synchronized RestrictionConfiguration createConfiguration(Context context) {
- if (configuration != null) {
- // This method is synchronized and another thread might already have created the configuration.
- return configuration;
- }
-
- if (isGuestProfile(context)) {
- return new GuestProfileConfiguration();
- } else if (isRestrictedProfile(context)) {
- return new RestrictedProfileConfiguration(context);
- } else {
- return new DefaultConfiguration();
- }
- }
-
- private static boolean isGuestProfile(Context context) {
- if (configuration != null) {
- return configuration instanceof GuestProfileConfiguration;
- }
-
- GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
- if (geckoInterface != null) {
- return geckoInterface.getProfile().inGuestMode();
- }
-
- return GeckoProfile.get(context).inGuestMode();
- }
-
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
- public static boolean isRestrictedProfile(Context context) {
- if (configuration != null) {
- return configuration instanceof RestrictedProfileConfiguration;
- }
-
- if (Versions.preJBMR2) {
- // Early versions don't support restrictions at all
- return false;
- }
-
- // The user is on a restricted profile if, and only if, we injected application restrictions during account setup.
- return RestrictionCache.hasApplicationRestrictions(context);
- }
-
- public static void update(Context context) {
- getConfiguration(context).update();
- }
-
- private static Restrictable geckoActionToRestriction(int action) {
- for (Restrictable rest : Restrictable.values()) {
- if (rest.id == action) {
- return rest;
- }
- }
-
- throw new IllegalArgumentException("Unknown action " + action);
- }
-
- private static boolean canLoadUrl(final Context context, final String url) {
- return getConfiguration(context).canLoadUrl(url);
- }
-
- @WrapForJNI
- public static boolean isUserRestricted() {
- return isUserRestricted(GeckoAppShell.getApplicationContext());
- }
-
- public static boolean isUserRestricted(final Context context) {
- return getConfiguration(context).isRestricted();
- }
-
- public static boolean isAllowed(final Context context, final Restrictable restrictable) {
- return getConfiguration(context).isAllowed(restrictable);
- }
-
- @WrapForJNI
- public static boolean isAllowed(int action, String url) {
- final Restrictable restrictable;
- try {
- restrictable = geckoActionToRestriction(action);
- } catch (IllegalArgumentException ex) {
- // Unknown actions represent a coding error, so we
- // refuse the action and log.
- Log.e(LOGTAG, "Unknown action " + action + "; check calling code.");
- return false;
- }
-
- final Context context = GeckoAppShell.getApplicationContext();
-
- if (Restrictable.BROWSE == restrictable) {
- return canLoadUrl(context, url);
- } else {
- return isAllowed(context, restrictable);
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.sqlite;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-
-/*
- * Helper class to make the ByteBuffers returned by SQLite BLOB
- * easier to use.
- */
-public class ByteBufferInputStream extends InputStream {
- private final ByteBuffer mByteBuffer;
-
- public ByteBufferInputStream(ByteBuffer aByteBuffer) {
- mByteBuffer = aByteBuffer;
- }
-
- @Override
- public synchronized int read() throws IOException {
- if (!mByteBuffer.hasRemaining()) {
- return -1;
- }
- return mByteBuffer.get();
- }
-
- @Override
- public synchronized int read(byte[] aBytes, int aOffset, int aLen)
- throws IOException {
- int toRead = Math.min(aLen, mByteBuffer.remaining());
- mByteBuffer.get(aBytes, aOffset, toRead);
- return toRead;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
+++ /dev/null
@@ -1,366 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/*
- * 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.sqlite;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants;
-
-import android.database.AbstractCursor;
-import android.database.CursorIndexOutOfBoundsException;
-import android.util.Log;
-
-/**
- * A mutable cursor implementation backed by an array of {@code Object}s. Use
- * {@link #newRow()} to add rows. Automatically expands internal capacity
- * as needed.
- *
- * This class provides one missing feature from Android's MatrixCursor:
- * the implementation of getBlob that was inadvertently omitted from API 9 (and
- * perhaps later; it's present in 14).
- *
- * MatrixCursor is all private, so we entirely duplicate it here.
- */
-public class MatrixBlobCursor extends AbstractCursor {
- private static final String LOGTAG = "GeckoMatrixCursor";
-
- private final String[] columnNames;
- private final int columnCount;
-
- private int rowCount;
- private Throwable allocationStack;
-
- Object[] data;
-
- /**
- * Constructs a new cursor with the given initial capacity.
- *
- * @param columnNames names of the columns, the ordering of which
- * determines column ordering elsewhere in this cursor
- * @param initialCapacity in rows
- */
- @WrapForJNI
- public MatrixBlobCursor(String[] columnNames, int initialCapacity) {
- this.columnNames = columnNames;
- this.columnCount = columnNames.length;
-
- if (initialCapacity < 1) {
- initialCapacity = 1;
- }
-
- this.data = new Object[columnCount * initialCapacity];
- if (AppConstants.DEBUG_BUILD) {
- this.allocationStack = new Throwable("allocationStack");
- }
- }
-
- /**
- * Constructs a new cursor.
- *
- * @param columnNames names of the columns, the ordering of which
- * determines column ordering elsewhere in this cursor
- */
- @WrapForJNI
- public MatrixBlobCursor(String[] columnNames) {
- this(columnNames, 16);
- }
-
- /**
- * Closes the Cursor, releasing all of its resources.
- */
- public void close() {
- this.allocationStack = null;
- this.data = null;
- super.close();
- }
-
- /**
- * Gets value at the given column for the current row.
- */
- protected Object get(int column) {
- if (column < 0 || column >= columnCount) {
- throw new CursorIndexOutOfBoundsException("Requested column: "
- + column + ", # of columns: " + columnCount);
- }
- if (mPos < 0) {
- throw new CursorIndexOutOfBoundsException("Before first row.");
- }
- if (mPos >= rowCount) {
- throw new CursorIndexOutOfBoundsException("After last row.");
- }
- return data[mPos * columnCount + column];
- }
-
- /**
- * Adds a new row to the end and returns a builder for that row. Not safe
- * for concurrent use.
- *
- * @return builder which can be used to set the column values for the new
- * row
- */
- public RowBuilder newRow() {
- rowCount++;
- int endIndex = rowCount * columnCount;
- ensureCapacity(endIndex);
- int start = endIndex - columnCount;
- return new RowBuilder(start, endIndex);
- }
-
- /**
- * Adds a new row to the end with the given column values. Not safe
- * for concurrent use.
- *
- * @throws IllegalArgumentException if {@code columnValues.length !=
- * columnNames.length}
- * @param columnValues in the same order as the the column names specified
- * at cursor construction time
- */
- @WrapForJNI
- public void addRow(Object[] columnValues) {
- if (columnValues.length != columnCount) {
- throw new IllegalArgumentException("columnNames.length = "
- + columnCount + ", columnValues.length = "
- + columnValues.length);
- }
-
- int start = rowCount++ * columnCount;
- ensureCapacity(start + columnCount);
- System.arraycopy(columnValues, 0, data, start, columnCount);
- }
-
- /**
- * Adds a new row to the end with the given column values. Not safe
- * for concurrent use.
- *
- * @throws IllegalArgumentException if {@code columnValues.size() !=
- * columnNames.length}
- * @param columnValues in the same order as the the column names specified
- * at cursor construction time
- */
- @WrapForJNI
- public void addRow(Iterable<?> columnValues) {
- final int start = rowCount * columnCount;
-
- if (columnValues instanceof ArrayList<?>) {
- addRow((ArrayList<?>) columnValues, start);
- return;
- }
-
- final int end = start + columnCount;
- int current = start;
-
- ensureCapacity(end);
- final Object[] localData = data;
- for (Object columnValue : columnValues) {
- if (current == end) {
- // TODO: null out row?
- throw new IllegalArgumentException(
- "columnValues.size() > columnNames.length");
- }
- localData[current++] = columnValue;
- }
-
- if (current != end) {
- // TODO: null out row?
- throw new IllegalArgumentException(
- "columnValues.size() < columnNames.length");
- }
-
- // Increase row count here in case we encounter an exception.
- rowCount++;
- }
-
- /** Optimization for {@link ArrayList}. */
- @WrapForJNI
- private void addRow(ArrayList<?> columnValues, int start) {
- final int size = columnValues.size();
- if (size != columnCount) {
- throw new IllegalArgumentException("columnNames.length = "
- + columnCount + ", columnValues.size() = " + size);
- }
-
- final int end = start + columnCount;
- ensureCapacity(end);
-
- // Take a reference just in case someone calls ensureCapacity
- // and `data` gets replaced by a new array!
- final Object[] localData = data;
- for (int i = 0; i < size; i++) {
- localData[start + i] = columnValues.get(i);
- }
-
- rowCount++;
- }
-
- /**
- * Ensures that this cursor has enough capacity. If it needs to allocate
- * a new array, the existing capacity will be at least doubled.
- */
- private void ensureCapacity(final int size) {
- if (size <= data.length) {
- return;
- }
-
- final Object[] oldData = this.data;
- this.data = new Object[Math.max(size, data.length * 2)];
- System.arraycopy(oldData, 0, this.data, 0, oldData.length);
- }
-
- /**
- * Builds a row, starting from the left-most column and adding one column
- * value at a time. Follows the same ordering as the column names specified
- * at cursor construction time.
- *
- * Not thread-safe.
- */
- public class RowBuilder {
- private int index;
- private final int endIndex;
-
- RowBuilder(int index, int endIndex) {
- this.index = index;
- this.endIndex = endIndex;
- }
-
- /**
- * Sets the next column value in this row.
- *
- * @throws CursorIndexOutOfBoundsException if you try to add too many
- * values
- * @return this builder to support chaining
- */
- public RowBuilder add(final Object columnValue) {
- if (index == endIndex) {
- throw new CursorIndexOutOfBoundsException("No more columns left.");
- }
-
- data[index++] = columnValue;
- return this;
- }
- }
-
- /**
- * Not thread safe.
- */
- public void set(int column, Object value) {
- if (column < 0 || column >= columnCount) {
- throw new CursorIndexOutOfBoundsException("Requested column: "
- + column + ", # of columns: " + columnCount);
- }
- if (mPos < 0) {
- throw new CursorIndexOutOfBoundsException("Before first row.");
- }
- if (mPos >= rowCount) {
- throw new CursorIndexOutOfBoundsException("After last row.");
- }
- data[mPos * columnCount + column] = value;
- }
-
- // AbstractCursor implementation.
- @Override
- public int getCount() {
- return rowCount;
- }
-
- @Override
- public String[] getColumnNames() {
- return columnNames;
- }
-
- @Override
- public String getString(int column) {
- Object value = get(column);
- if (value == null) return null;
- return value.toString();
- }
-
- @Override
- public short getShort(int column) {
- final Object value = get(column);
- if (value == null) return 0;
- if (value instanceof Number) return ((Number) value).shortValue();
- return Short.parseShort(value.toString());
- }
-
- @Override
- public int getInt(int column) {
- Object value = get(column);
- if (value == null) return 0;
- if (value instanceof Number) return ((Number) value).intValue();
- return Integer.parseInt(value.toString());
- }
-
- @Override
- public long getLong(int column) {
- Object value = get(column);
- if (value == null) return 0;
- if (value instanceof Number) return ((Number) value).longValue();
- return Long.parseLong(value.toString());
- }
-
- @Override
- public float getFloat(int column) {
- Object value = get(column);
- if (value == null) return 0.0f;
- if (value instanceof Number) return ((Number) value).floatValue();
- return Float.parseFloat(value.toString());
- }
-
- @Override
- public double getDouble(int column) {
- Object value = get(column);
- if (value == null) return 0.0d;
- if (value instanceof Number) return ((Number) value).doubleValue();
- return Double.parseDouble(value.toString());
- }
-
- @Override
- public byte[] getBlob(int column) {
- Object value = get(column);
- if (value == null) return null;
- if (value instanceof byte[]) {
- return (byte[]) value;
- }
-
- if (value instanceof ByteBuffer) {
- final ByteBuffer bytes = (ByteBuffer) value;
- byte[] byteArray = new byte[bytes.remaining()];
- bytes.get(byteArray);
- return byteArray;
- }
- throw new UnsupportedOperationException("BLOB Object not of known type");
- }
-
- @Override
- public boolean isNull(int column) {
- return get(column) == null;
- }
-
- @Override
- protected void finalize() {
- if (AppConstants.DEBUG_BUILD) {
- if (!isClosed()) {
- Log.e(LOGTAG, "Cursor finalized without being closed", this.allocationStack);
- }
- }
-
- super.finalize();
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
+++ /dev/null
@@ -1,387 +0,0 @@
-/* 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.sqlite;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Map.Entry;
-
-/*
- * This class allows using the mozsqlite3 library included with Firefox
- * to read SQLite databases, instead of the Android SQLiteDataBase API,
- * which might use whatever outdated DB is present on the Android system.
- */
-public class SQLiteBridge {
- private static final String LOGTAG = "SQLiteBridge";
-
- // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
- private final String mDb;
-
- // Pointer to the database if it was opened with openDatabase. 0 implies closed.
- protected volatile long mDbPointer;
-
- // Values remembered after a query.
- private long[] mQueryResults;
-
- private boolean mTransactionSuccess;
- private boolean mInTransaction;
-
- private static final int RESULT_INSERT_ROW_ID = 0;
- private static final int RESULT_ROWS_CHANGED = 1;
-
- // Shamelessly cribbed from db/sqlite3/src/moz.build.
- private static final int DEFAULT_PAGE_SIZE_BYTES = 32768;
-
- // The same size we use elsewhere.
- private static final int MAX_WAL_SIZE_BYTES = 524288;
-
- // JNI code in $(topdir)/mozglue/android/..
- private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery,
- String[] aParams,
- long[] aUpdateResult)
- throws SQLiteBridgeException;
- private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery,
- String[] aParams,
- long[] aUpdateResult)
- throws SQLiteBridgeException;
- private static native long openDatabase(String aDb)
- throws SQLiteBridgeException;
- private static native void closeDatabase(long aDb);
-
- // Takes the path to the database we want to access.
- @RobocopTarget
- public SQLiteBridge(String aDb) throws SQLiteBridgeException {
- mDb = aDb;
- }
-
- // Executes a simple line of sql.
- public void execSQL(String sql)
- throws SQLiteBridgeException {
- Cursor cursor = internalQuery(sql, null);
- cursor.close();
- }
-
- // Executes a simple line of sql. Allow you to bind arguments
- public void execSQL(String sql, String[] bindArgs)
- throws SQLiteBridgeException {
- Cursor cursor = internalQuery(sql, bindArgs);
- cursor.close();
- }
-
- // Executes a DELETE statement on the database
- public int delete(String table, String whereClause, String[] whereArgs)
- throws SQLiteBridgeException {
- StringBuilder sb = new StringBuilder("DELETE from ");
- sb.append(table);
- if (whereClause != null) {
- sb.append(" WHERE " + whereClause);
- }
-
- execSQL(sb.toString(), whereArgs);
- return (int)mQueryResults[RESULT_ROWS_CHANGED];
- }
-
- public Cursor query(String table,
- String[] columns,
- String selection,
- String[] selectionArgs,
- String groupBy,
- String having,
- String orderBy,
- String limit)
- throws SQLiteBridgeException {
- StringBuilder sb = new StringBuilder("SELECT ");
- if (columns != null)
- sb.append(TextUtils.join(", ", columns));
- else
- sb.append(" * ");
-
- sb.append(" FROM ");
- sb.append(table);
-
- if (selection != null) {
- sb.append(" WHERE " + selection);
- }
-
- if (groupBy != null) {
- sb.append(" GROUP BY " + groupBy);
- }
-
- if (having != null) {
- sb.append(" HAVING " + having);
- }
-
- if (orderBy != null) {
- sb.append(" ORDER BY " + orderBy);
- }
-
- if (limit != null) {
- sb.append(" " + limit);
- }
-
- return rawQuery(sb.toString(), selectionArgs);
- }
-
- @RobocopTarget
- public Cursor rawQuery(String sql, String[] selectionArgs)
- throws SQLiteBridgeException {
- return internalQuery(sql, selectionArgs);
- }
-
- public long insert(String table, String nullColumnHack, ContentValues values)
- throws SQLiteBridgeException {
- if (values == null)
- return 0;
-
- ArrayList<String> valueNames = new ArrayList<String>();
- ArrayList<String> valueBinds = new ArrayList<String>();
- ArrayList<String> keyNames = new ArrayList<String>();
-
- for (Entry<String, Object> value : values.valueSet()) {
- keyNames.add(value.getKey());
-
- Object val = value.getValue();
- if (val == null) {
- valueNames.add("NULL");
- } else {
- valueNames.add("?");
- valueBinds.add(val.toString());
- }
- }
-
- StringBuilder sb = new StringBuilder("INSERT into ");
- sb.append(table);
-
- sb.append(" (");
- sb.append(TextUtils.join(", ", keyNames));
- sb.append(")");
-
- // XXX - Do we need to bind these values?
- sb.append(" VALUES (");
- sb.append(TextUtils.join(", ", valueNames));
- sb.append(") ");
-
- String[] binds = new String[valueBinds.size()];
- valueBinds.toArray(binds);
- execSQL(sb.toString(), binds);
- return mQueryResults[RESULT_INSERT_ROW_ID];
- }
-
- public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
- throws SQLiteBridgeException {
- if (values == null)
- return 0;
-
- ArrayList<String> valueNames = new ArrayList<String>();
-
- StringBuilder sb = new StringBuilder("UPDATE ");
- sb.append(table);
- sb.append(" SET ");
-
- boolean isFirst = true;
-
- for (Entry<String, Object> value : values.valueSet()) {
- if (isFirst)
- isFirst = false;
- else
- sb.append(", ");
-
- sb.append(value.getKey());
-
- Object val = value.getValue();
- if (val == null) {
- sb.append(" = NULL");
- } else {
- sb.append(" = ?");
- valueNames.add(val.toString());
- }
- }
-
- if (!TextUtils.isEmpty(whereClause)) {
- sb.append(" WHERE ");
- sb.append(whereClause);
- valueNames.addAll(Arrays.asList(whereArgs));
- }
-
- String[] binds = new String[valueNames.size()];
- valueNames.toArray(binds);
-
- execSQL(sb.toString(), binds);
- return (int)mQueryResults[RESULT_ROWS_CHANGED];
- }
-
- public int getVersion()
- throws SQLiteBridgeException {
- Cursor cursor = internalQuery("PRAGMA user_version", null);
- int ret = -1;
- if (cursor != null) {
- cursor.moveToFirst();
- String version = cursor.getString(0);
- ret = Integer.parseInt(version);
- cursor.close();
- }
- return ret;
- }
-
- // Do an SQL query, substituting the parameters in the query with the passed
- // parameters. The parameters are substituted in order: named parameters
- // are not supported.
- private Cursor internalQuery(String aQuery, String[] aParams)
- throws SQLiteBridgeException {
-
- mQueryResults = new long[2];
- if (isOpen()) {
- return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults);
- }
- return sqliteCall(mDb, aQuery, aParams, mQueryResults);
- }
-
- /*
- * The second two parameters here are just provided for compatibility with SQLiteDatabase
- * Support for them is not currently implemented.
- */
- public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
- throws SQLiteException {
- if (factory != null) {
- throw new RuntimeException("factory not supported.");
- }
- if (flags != 0) {
- throw new RuntimeException("flags not supported.");
- }
-
- SQLiteBridge bridge = null;
- try {
- bridge = new SQLiteBridge(path);
- bridge.mDbPointer = SQLiteBridge.openDatabase(path);
- } catch (SQLiteBridgeException ex) {
- // Catch and rethrow as a SQLiteException to match SQLiteDatabase.
- throw new SQLiteException(ex.getMessage());
- }
-
- prepareWAL(bridge);
-
- return bridge;
- }
-
- public void close() {
- if (isOpen()) {
- closeDatabase(mDbPointer);
- }
- mDbPointer = 0L;
- }
-
- public boolean isOpen() {
- return mDbPointer != 0;
- }
-
- public void beginTransaction() throws SQLiteBridgeException {
- if (inTransaction()) {
- throw new SQLiteBridgeException("Nested transactions are not supported");
- }
- execSQL("BEGIN EXCLUSIVE");
- mTransactionSuccess = false;
- mInTransaction = true;
- }
-
- public void beginTransactionNonExclusive() throws SQLiteBridgeException {
- if (inTransaction()) {
- throw new SQLiteBridgeException("Nested transactions are not supported");
- }
- execSQL("BEGIN IMMEDIATE");
- mTransactionSuccess = false;
- mInTransaction = true;
- }
-
- public void endTransaction() {
- if (!inTransaction())
- return;
-
- try {
- if (mTransactionSuccess) {
- execSQL("COMMIT TRANSACTION");
- } else {
- execSQL("ROLLBACK TRANSACTION");
- }
- } catch (SQLiteBridgeException ex) {
- Log.e(LOGTAG, "Error ending transaction", ex);
- }
- mInTransaction = false;
- mTransactionSuccess = false;
- }
-
- public void setTransactionSuccessful() throws SQLiteBridgeException {
- if (!inTransaction()) {
- throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction");
- }
- mTransactionSuccess = true;
- }
-
- public boolean inTransaction() {
- return mInTransaction;
- }
-
- @Override
- public void finalize() {
- if (isOpen()) {
- Log.e(LOGTAG, "Bridge finalized without closing the database");
- close();
- }
- }
-
- private static void prepareWAL(final SQLiteBridge bridge) {
- // Prepare for WAL mode. If we can, we switch to journal_mode=WAL, then
- // set the checkpoint size appropriately. If we can't, then we fall back
- // to truncating and synchronous writes.
- final Cursor cursor = bridge.internalQuery("PRAGMA journal_mode=WAL", null);
- try {
- if (cursor.moveToFirst()) {
- String journalMode = cursor.getString(0);
- Log.d(LOGTAG, "Journal mode: " + journalMode);
- if ("wal".equals(journalMode)) {
- // Success! Let's make sure we autocheckpoint at a reasonable interval.
- final int pageSizeBytes = bridge.getPageSizeBytes();
- final int checkpointPageCount = MAX_WAL_SIZE_BYTES / pageSizeBytes;
- bridge.execSQL("PRAGMA wal_autocheckpoint=" + checkpointPageCount);
- } else {
- if (!"truncate".equals(journalMode)) {
- Log.w(LOGTAG, "Unable to activate WAL journal mode. Using truncate instead.");
- bridge.execSQL("PRAGMA journal_mode=TRUNCATE");
- }
- Log.w(LOGTAG, "Not using WAL mode: using synchronous=FULL instead.");
- bridge.execSQL("PRAGMA synchronous=FULL");
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- private int getPageSizeBytes() {
- if (!isOpen()) {
- throw new IllegalStateException("Database not open.");
- }
-
- final Cursor cursor = internalQuery("PRAGMA page_size", null);
- try {
- if (!cursor.moveToFirst()) {
- Log.w(LOGTAG, "Unable to retrieve page size.");
- return DEFAULT_PAGE_SIZE_BYTES;
- }
-
- return cursor.getInt(0);
- } finally {
- cursor.close();
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
+++ /dev/null
@@ -1,18 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.sqlite;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-@WrapForJNI
-public class SQLiteBridgeException extends RuntimeException {
- static final long serialVersionUID = 1L;
-
- public SQLiteBridgeException() {}
- public SQLiteBridgeException(String msg) {
- super(msg);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ActivityResultHandler.java
+++ /dev/null
@@ -1,11 +0,0 @@
-/* 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.util;
-
-import android.content.Intent;
-
-public interface ActivityResultHandler {
- void onActivityResult(int resultCode, Intent data);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* 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.util;
-
-import android.util.SparseArray;
-
-public final class ActivityResultHandlerMap {
- private final SparseArray<ActivityResultHandler> mMap = new SparseArray<ActivityResultHandler>();
- private int mCounter;
-
- public synchronized int put(ActivityResultHandler handler) {
- mMap.put(mCounter, handler);
- return mCounter++;
- }
-
- public synchronized ActivityResultHandler getAndRemove(int i) {
- ActivityResultHandler handler = mMap.get(i);
- mMap.delete(i);
-
- return handler;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ActivityUtils.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- 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.util;
-
-import android.app.Activity;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-public class ActivityUtils {
- private ActivityUtils() {
- }
-
- public static void setFullScreen(Activity activity, boolean fullscreen) {
- // Hide/show the system notification bar
- Window window = activity.getWindow();
-
- if (Versions.feature16Plus) {
- final int newVis;
- if (fullscreen) {
- newVis = View.SYSTEM_UI_FLAG_FULLSCREEN |
- View.SYSTEM_UI_FLAG_LOW_PROFILE;
- } else {
- newVis = View.SYSTEM_UI_FLAG_VISIBLE;
- }
-
- window.getDecorView().setSystemUiVisibility(newVis);
- } else {
- window.setFlags(fullscreen ?
- WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
- WindowManager.LayoutParams.FLAG_FULLSCREEN);
- }
- }
-
- public static boolean isFullScreen(final Activity activity) {
- final Window window = activity.getWindow();
-
- if (Versions.feature16Plus) {
- final int vis = window.getDecorView().getSystemUiVisibility();
- return (vis & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
- }
-
- final int flags = window.getAttributes().flags;
- return ((flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/BundleEventListener.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import android.os.Bundle;
-
-@RobocopTarget
-public interface BundleEventListener {
- /**
- * Handles a message sent from Gecko.
- *
- * @param event The name of the event being sent.
- * @param message The message data.
- * @param callback The callback interface for this message. A callback is provided only if the
- * originating Messaging.sendRequest call included a callback argument;
- * otherwise, callback will be null. All listeners for a given event are given
- * the same callback object, and exactly one listener must handle the callback.
- */
- void handleMessage(String event, Bundle message, EventCallback callback);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/Clipboard.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/* 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.util;
-
-import java.util.concurrent.SynchronousQueue;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.content.ClipData;
-import android.content.Context;
-import android.util.Log;
-
-public final class Clipboard {
- // Volatile but not synchronized: we don't care about the race condition in
- // init, because both app contexts will be the same, but we do care about a
- // thread having a stale null value of mContext.
- volatile static Context mContext;
- private final static String LOGTAG = "GeckoClipboard";
- private final static SynchronousQueue<String> sClipboardQueue = new SynchronousQueue<String>();
-
- private Clipboard() {
- }
-
- public static void init(final Context c) {
- if (mContext != null) {
- Log.w(LOGTAG, "Clipboard.init() called twice!");
- return;
- }
- mContext = c.getApplicationContext();
- }
-
- @WrapForJNI(stubName = "GetClipboardTextWrapper")
- public static String getText() {
- // If we're on the UI thread or the background thread, we have a looper on the thread
- // and can just call this directly. For any other threads, post the call to the
- // background thread.
-
- if (ThreadUtils.isOnUiThread() || ThreadUtils.isOnBackgroundThread()) {
- return getClipboardTextImpl();
- }
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- String text = getClipboardTextImpl();
- try {
- sClipboardQueue.put(text != null ? text : "");
- } catch (InterruptedException ie) { }
- }
- });
-
- try {
- return sClipboardQueue.take();
- } catch (InterruptedException ie) {
- return "";
- }
- }
-
- @WrapForJNI(stubName = "SetClipboardText")
- public static void setText(final CharSequence text) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- @SuppressWarnings("deprecation")
- public void run() {
- // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
- // which is a subclass of android.text.ClipboardManager.
- if (Versions.feature11Plus) {
- final android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
- final ClipData clip = ClipData.newPlainText("Text", text);
- try {
- cm.setPrimaryClip(clip);
- } catch (NullPointerException e) {
- // Bug 776223: This is a Samsung clipboard bug. setPrimaryClip() can throw
- // a NullPointerException if Samsung's /data/clipboard directory is full.
- // Fortunately, the text is still successfully copied to the clipboard.
- }
- return;
- }
-
- // Deprecated.
- android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
- cm.setText(text);
- }
- });
- }
-
- /**
- * @return true if the clipboard is nonempty, false otherwise.
- */
- @WrapForJNI
- public static boolean hasText() {
- if (Versions.feature11Plus) {
- android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
- return cm.hasPrimaryClip();
- }
-
- // Deprecated.
- android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
- return cm.hasText();
- }
-
- /**
- * Deletes all text from the clipboard.
- */
- @WrapForJNI
- public static void clearText() {
- setText(null);
- }
-
- /**
- * On some devices, access to the clipboard service needs to happen
- * on a thread with a looper, so this function requires a looper is
- * present on the thread.
- */
- @SuppressWarnings("deprecation")
- static String getClipboardTextImpl() {
- if (Versions.feature11Plus) {
- android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
- if (cm.hasPrimaryClip()) {
- ClipData clip = cm.getPrimaryClip();
- if (clip != null) {
- ClipData.Item item = clip.getItemAt(0);
- return item.coerceToText(mContext).toString();
- }
- }
- } else {
- android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
- if (cm.hasText()) {
- return cm.getText().toString();
- }
- }
- return null;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ContextUtils.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.util;
-
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-
-public class ContextUtils {
- private ContextUtils() {}
-
- /**
- * @return {@link android.content.pm.PackageInfo#firstInstallTime} for the context's package.
- * @throws PackageManager.NameNotFoundException Unexpected - we get the package name from the context so
- * it's expected to be found.
- */
- public static PackageInfo getCurrentPackageInfo(final Context context) {
- try {
- return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
- } catch (PackageManager.NameNotFoundException e) {
- throw new AssertionError("Should not happen: Can't get package info of own package");
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/DateUtil.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.util;
-
-import android.support.annotation.NonNull;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Utilities to help with manipulating Java's dates and calendars.
- */
-public class DateUtil {
- private DateUtil() {}
-
- /**
- * @param date the date to convert to HTTP format
- * @return the date as specified in rfc 1123, e.g. "Tue, 01 Feb 2011 14:00:00 GMT"
- */
- public static String getDateInHTTPFormat(@NonNull final Date date) {
- final DateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.US);
- df.setTimeZone(TimeZone.getTimeZone("GMT"));
- return df.format(date);
- }
-
- /**
- * Returns the timezone offset for the current date in minutes. See
- * {@link #getTimezoneOffsetInMinutesForGivenDate(Calendar)} for more details.
- */
- public static int getTimezoneOffsetInMinutes(@NonNull final TimeZone timezone) {
- return getTimezoneOffsetInMinutesForGivenDate(Calendar.getInstance(timezone));
- }
-
- /**
- * Returns the time zone offset for the given date in minutes. The date makes a difference due to daylight
- * savings time in some regions. We return minutes because we can accurately represent time zones that are
- * offset by non-integer hour values, e.g. parts of New Zealand at UTC+12:45.
- *
- * @param calendar A calendar with the appropriate time zone & date already set.
- */
- public static int getTimezoneOffsetInMinutesForGivenDate(@NonNull final Calendar calendar) {
- // via Date.getTimezoneOffset deprecated docs (note: it had incorrect order of operations).
- // Also, we cast to int because we should never overflow here - the max should be GMT+14 = 840.
- return (int) TimeUnit.MILLISECONDS.toMinutes(calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET));
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.CheckResult;
-import android.support.annotation.ColorInt;
-import android.support.annotation.ColorRes;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-
-public class DrawableUtil {
-
- /**
- * Tints the given drawable with the given color and returns it.
- */
- @CheckResult
- public static Drawable tintDrawable(@NonNull final Context context,
- @DrawableRes final int drawableID,
- @ColorInt final int color) {
- final Drawable icon = DrawableCompat.wrap(
- ContextCompat.getDrawable(context, drawableID).mutate());
- DrawableCompat.setTint(icon, color);
- return icon;
- }
-
- /**
- * Tints the given drawable with the given color and returns it.
- */
- @CheckResult
- public static Drawable tintDrawableWithColorRes(@NonNull final Context context,
- @DrawableRes final int drawableID,
- @ColorRes final int colorID) {
- return tintDrawable(context, drawableID, ContextCompat.getColor(context, colorID));
- }
-
- /**
- * Tints the given drawable with the given tint list and returns it. Note that you
- * should no longer use the argument Drawable because the argument is not mutated
- * on pre-Lollipop devices but is mutated on L+ due to differences in the Support
- * Library implementation (bug 1193950).
- */
- @CheckResult
- public static Drawable tintDrawableWithStateList(@NonNull final Drawable drawable,
- @NonNull final ColorStateList colorList) {
- final Drawable wrappedDrawable = DrawableCompat.wrap(drawable.mutate());
- DrawableCompat.setTintList(wrappedDrawable, colorList);
- return wrappedDrawable;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/EventCallback.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.mozilla.gecko.util;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-/**
- * Callback interface for Gecko requests.
- *
- * For each instance of EventCallback, exactly one of sendResponse, sendError, or sendCancel
- * must be called to prevent observer leaks. If more than one send* method is called, or if a
- * single send method is called multiple times, an {@link IllegalStateException} will be thrown.
- */
-@RobocopTarget
-public interface EventCallback {
- /**
- * Sends a success response with the given data.
- *
- * @param response The response data to send to Gecko. Can be any of the types accepted by
- * JSONObject#put(String, Object).
- */
- public void sendSuccess(Object response);
-
- /**
- * Sends an error response with the given data.
- *
- * @param response The response data to send to Gecko. Can be any of the types accepted by
- * JSONObject#put(String, Object).
- */
- public void sendError(Object response);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/FileUtils.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/* 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.util;
-
-import android.util.Log;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.FilenameFilter;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.nio.charset.Charset;
-import java.util.Comparator;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-public class FileUtils {
- private static final String LOGTAG = "GeckoFileUtils";
-
- /*
- * A basic Filter for checking a filename and age.
- **/
- static public class NameAndAgeFilter implements FilenameFilter {
- final private String mName;
- final private double mMaxAge;
-
- public NameAndAgeFilter(String name, double age) {
- mName = name;
- mMaxAge = age;
- }
-
- @Override
- public boolean accept(File dir, String filename) {
- if (mName == null || mName.matches(filename)) {
- File f = new File(dir, filename);
-
- if (mMaxAge < 0 || System.currentTimeMillis() - f.lastModified() > mMaxAge) {
- return true;
- }
- }
-
- return false;
- }
- }
-
- @RobocopTarget
- public static void delTree(File dir, FilenameFilter filter, boolean recurse) {
- String[] files = null;
-
- if (filter != null) {
- files = dir.list(filter);
- } else {
- files = dir.list();
- }
-
- if (files == null) {
- return;
- }
-
- for (String file : files) {
- File f = new File(dir, file);
- delete(f, recurse);
- }
- }
-
- public static boolean delete(File file) throws IOException {
- return delete(file, true);
- }
-
- public static boolean delete(File file, boolean recurse) {
- if (file.isDirectory() && recurse) {
- // If the quick delete failed and this is a dir, recursively delete the contents of the dir
- String files[] = file.list();
- for (String temp : files) {
- File fileDelete = new File(file, temp);
- try {
- delete(fileDelete);
- } catch (IOException ex) {
- Log.i(LOGTAG, "Error deleting " + fileDelete.getPath(), ex);
- }
- }
- }
-
- // Even if this is a dir, it should now be empty and delete should work
- return file.delete();
- }
-
- /**
- * A generic solution to read a JSONObject from a file. See
- * {@link #readStringFromFile(File)} for more details.
- *
- * @throws IOException if the file is empty, or another IOException occurs
- * @throws JSONException if the file could not be converted to a JSONObject.
- */
- public static JSONObject readJSONObjectFromFile(final File file) throws IOException, JSONException {
- if (file.length() == 0) {
- // Redirect this exception so it's clearer than when the JSON parser catches it.
- throw new IOException("Given file is empty - the JSON parser cannot create an object from an empty file");
- }
- return new JSONObject(readStringFromFile(file));
- }
-
- /**
- * A generic solution to read from a file. For more details,
- * see {@link #readStringFromInputStreamAndCloseStream(InputStream, int)}.
- *
- * This method loads the entire file into memory so will have the expected performance impact.
- * If you're trying to read a large file, you should be handling your own reading to avoid
- * out-of-memory errors.
- */
- public static String readStringFromFile(final File file) throws IOException {
- // FileInputStream will throw FileNotFoundException if the file does not exist, but
- // File.length will return 0 if the file does not exist so we catch it sooner.
- if (!file.exists()) {
- throw new FileNotFoundException("Given file, " + file + ", does not exist");
- } else if (file.length() == 0) {
- return "";
- }
- final int len = (int) file.length(); // includes potential EOF character.
- return readStringFromInputStreamAndCloseStream(new FileInputStream(file), len);
- }
-
- /**
- * A generic solution to read from an input stream in UTF-8. This function will read from the stream until it
- * is finished and close the stream - this is necessary to close the wrapping resources.
- *
- * For a higher-level method, see {@link #readStringFromFile(File)}.
- *
- * Since this is generic, it may not be the most performant for your use case.
- *
- * @param bufferSize Size of the underlying buffer for read optimizations - must be > 0.
- */
- public static String readStringFromInputStreamAndCloseStream(final InputStream inputStream, final int bufferSize)
- throws IOException {
- if (bufferSize <= 0) {
- // Safe close: it's more important to alert the programmer of
- // their error than to let them catch and continue on their way.
- IOUtils.safeStreamClose(inputStream);
- throw new IllegalArgumentException("Expected buffer size larger than 0. Got: " + bufferSize);
- }
-
- final StringBuilder stringBuilder = new StringBuilder(bufferSize);
- final InputStreamReader reader = new InputStreamReader(inputStream, Charset.forName("UTF-8"));
- try {
- int charsRead;
- final char[] buffer = new char[bufferSize];
- while ((charsRead = reader.read(buffer, 0, bufferSize)) != -1) {
- stringBuilder.append(buffer, 0, charsRead);
- }
- } finally {
- reader.close();
- }
- return stringBuilder.toString();
- }
-
- /**
- * A generic solution to write a JSONObject to a file.
- * See {@link #writeStringToFile(File, String)} for more details.
- */
- public static void writeJSONObjectToFile(final File file, final JSONObject obj) throws IOException {
- writeStringToFile(file, obj.toString());
- }
-
- /**
- * A generic solution to write to a File - the given file will be overwritten. If it does not exist yet, it will
- * be created. See {@link #writeStringToOutputStreamAndCloseStream(OutputStream, String)} for more details.
- */
- public static void writeStringToFile(final File file, final String str) throws IOException {
- writeStringToOutputStreamAndCloseStream(new FileOutputStream(file, false), str);
- }
-
- /**
- * A generic solution to write to an output stream in UTF-8. The stream will be closed at the
- * completion of this method - it's necessary in order to close the wrapping resources.
- *
- * For a higher-level method, see {@link #writeStringToFile(File, String)}.
- *
- * Since this is generic, it may not be the most performant for your use case.
- */
- public static void writeStringToOutputStreamAndCloseStream(final OutputStream outputStream, final String str)
- throws IOException {
- try {
- final OutputStreamWriter writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"));
- try {
- writer.write(str);
- } finally {
- writer.close();
- }
- } finally {
- // OutputStreamWriter.close can throw before closing the
- // underlying stream. For safety, we close here too.
- outputStream.close();
- }
- }
-
- public static class FilenameWhitelistFilter implements FilenameFilter {
- private final Set<String> mFilenameWhitelist;
-
- public FilenameWhitelistFilter(final Set<String> filenameWhitelist) {
- mFilenameWhitelist = filenameWhitelist;
- }
-
- @Override
- public boolean accept(final File dir, final String filename) {
- return mFilenameWhitelist.contains(filename);
- }
- }
-
- public static class FilenameRegexFilter implements FilenameFilter {
- private final Pattern mPattern;
-
- // Each time `Pattern.matcher` is called, a new matcher is created. We can avoid the excessive object creation
- // by caching the returned matcher and calling `Matcher.reset` on it. Since Matcher's are not thread safe,
- // this assumes `FilenameFilter.accept` is not run in parallel (which, according to the source, it is not).
- private Matcher mCachedMatcher;
-
- public FilenameRegexFilter(final Pattern pattern) {
- mPattern = pattern;
- }
-
- @Override
- public boolean accept(final File dir, final String filename) {
- if (mCachedMatcher == null) {
- mCachedMatcher = mPattern.matcher(filename);
- } else {
- mCachedMatcher.reset(filename);
- }
- return mCachedMatcher.matches();
- }
- }
-
- public static class FileLastModifiedComparator implements Comparator<File> {
- @Override
- public int compare(final File lhs, final File rhs) {
- // Long.compare is API 19+.
- final long lhsModified = lhs.lastModified();
- final long rhsModified = rhs.lastModified();
- if (lhsModified < rhsModified) {
- return -1;
- } else if (lhsModified == rhsModified) {
- return 0;
- } else {
- return 1;
- }
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/FloatUtils.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.graphics.PointF;
-
-import java.lang.IllegalArgumentException;
-
-public final class FloatUtils {
- private FloatUtils() {}
-
- public static boolean fuzzyEquals(float a, float b) {
- return (Math.abs(a - b) < 1e-6);
- }
-
- public static boolean fuzzyEquals(PointF a, PointF b) {
- return fuzzyEquals(a.x, b.x) && fuzzyEquals(a.y, b.y);
- }
-
- /*
- * Returns the value that represents a linear transition between `from` and `to` at time `t`,
- * which is on the scale [0, 1). Thus with t = 0.0f, this returns `from`; with t = 1.0f, this
- * returns `to`; with t = 0.5f, this returns the value halfway from `from` to `to`.
- */
- public static float interpolate(float from, float to, float t) {
- return from + (to - from) * t;
- }
-
- /**
- * Returns 'value', clamped so that it isn't any lower than 'low', and it
- * isn't any higher than 'high'.
- */
- public static float clamp(float value, float low, float high) {
- if (high < low) {
- throw new IllegalArgumentException(
- "clamp called with invalid parameters (" + high + " < " + low + ")" );
- }
- return Math.max(low, Math.min(high, value));
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GamepadUtils.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.annotation.TargetApi;
-import android.os.Build;
-import android.view.InputDevice;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-
-public final class GamepadUtils {
- private static final int SONY_XPERIA_GAMEPAD_DEVICE_ID = 196611;
-
- private static View.OnKeyListener sClickDispatcher;
- private static float sDeadZoneThresholdOverride = 1e-2f;
-
- private GamepadUtils() {
- }
-
- @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
- private static boolean isGamepadKey(KeyEvent event) {
- if (Build.VERSION.SDK_INT < 12) {
- return false;
- }
- return (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
- }
-
- public static boolean isActionKey(KeyEvent event) {
- return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_A));
- }
-
- public static boolean isActionKeyDown(KeyEvent event) {
- return isActionKey(event) && event.getAction() == KeyEvent.ACTION_DOWN;
- }
-
- public static boolean isBackKey(KeyEvent event) {
- return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B));
- }
-
- public static void overrideDeadZoneThreshold(float threshold) {
- sDeadZoneThresholdOverride = threshold;
- }
-
- public static boolean isValueInDeadZone(MotionEvent event, int axis) {
- float threshold;
- if (sDeadZoneThresholdOverride >= 0) {
- threshold = sDeadZoneThresholdOverride;
- } else {
- InputDevice.MotionRange range = event.getDevice().getMotionRange(axis);
- threshold = range.getFlat() + range.getFuzz();
- }
- float value = event.getAxisValue(axis);
- return (Math.abs(value) < threshold);
- }
-
- public static boolean isPanningControl(MotionEvent event) {
- if (Build.VERSION.SDK_INT < 12) {
- return false;
- }
- if ((event.getSource() & InputDevice.SOURCE_CLASS_MASK) != InputDevice.SOURCE_CLASS_JOYSTICK) {
- return false;
- }
- if (isValueInDeadZone(event, MotionEvent.AXIS_X)
- && isValueInDeadZone(event, MotionEvent.AXIS_Y)
- && isValueInDeadZone(event, MotionEvent.AXIS_Z)
- && isValueInDeadZone(event, MotionEvent.AXIS_RZ)) {
- return false;
- }
- return true;
- }
-
- public static View.OnKeyListener getClickDispatcher() {
- if (sClickDispatcher == null) {
- sClickDispatcher = new View.OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (isActionKeyDown(event)) {
- return v.performClick();
- }
- return false;
- }
- };
- }
- return sClickDispatcher;
- }
-
- public static KeyEvent translateSonyXperiaGamepadKeys(int keyCode, KeyEvent event) {
- // The cross and circle button mappings may be swapped in the different regions so
- // determine if they are swapped so the proper key codes can be mapped to the keys
- boolean areKeysSwapped = areSonyXperiaGamepadKeysSwapped();
-
- // If a Sony Xperia, remap the cross and circle buttons to buttons
- // A and B for the gamepad API
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- keyCode = (areKeysSwapped ? KeyEvent.KEYCODE_BUTTON_A : KeyEvent.KEYCODE_BUTTON_B);
- break;
-
- case KeyEvent.KEYCODE_DPAD_CENTER:
- keyCode = (areKeysSwapped ? KeyEvent.KEYCODE_BUTTON_B : KeyEvent.KEYCODE_BUTTON_A);
- break;
-
- default:
- return event;
- }
-
- return new KeyEvent(event.getAction(), keyCode);
- }
-
- public static boolean isSonyXperiaGamepadKeyEvent(KeyEvent event) {
- return (event.getDeviceId() == SONY_XPERIA_GAMEPAD_DEVICE_ID &&
- "Sony Ericsson".equals(Build.MANUFACTURER) &&
- ("R800".equals(Build.MODEL) || "R800i".equals(Build.MODEL)));
- }
-
- private static boolean areSonyXperiaGamepadKeysSwapped() {
- // The cross and circle buttons on Sony Xperia phones are swapped
- // in different regions
- // http://developer.sonymobile.com/2011/02/13/xperia-play-game-keys/
- final char DEFAULT_O_BUTTON_LABEL = 0x25CB;
-
- boolean swapped = false;
- int[] deviceIds = InputDevice.getDeviceIds();
-
- for (int i = 0; deviceIds != null && i < deviceIds.length; i++) {
- KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(deviceIds[i]);
- if (keyCharacterMap != null && DEFAULT_O_BUTTON_LABEL ==
- keyCharacterMap.getDisplayLabel(KeyEvent.KEYCODE_DPAD_CENTER)) {
- swapped = true;
- break;
- }
- }
- return swapped;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/* 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.util;
-
-import android.os.Handler;
-import android.os.Looper;
-
-import java.util.concurrent.SynchronousQueue;
-
-final class GeckoBackgroundThread extends Thread {
- private static final String LOOPER_NAME = "GeckoBackgroundThread";
-
- // Guarded by 'GeckoBackgroundThread.class'.
- private static Handler handler;
- private static Thread thread;
-
- // The initial Runnable to run on the new thread. Its purpose
- // is to avoid us having to wait for the new thread to start.
- private Runnable initialRunnable;
-
- // Singleton, so private constructor.
- private GeckoBackgroundThread(final Runnable initialRunnable) {
- this.initialRunnable = initialRunnable;
- }
-
- @Override
- public void run() {
- setName(LOOPER_NAME);
- Looper.prepare();
-
- synchronized (GeckoBackgroundThread.class) {
- handler = new Handler();
- GeckoBackgroundThread.class.notify();
- }
-
- if (initialRunnable != null) {
- initialRunnable.run();
- initialRunnable = null;
- }
-
- Looper.loop();
- }
-
- private static void startThread(final Runnable initialRunnable) {
- thread = new GeckoBackgroundThread(initialRunnable);
- ThreadUtils.setBackgroundThread(thread);
-
- thread.setDaemon(true);
- thread.start();
- }
-
- // Get a Handler for a looper thread, or create one if it doesn't yet exist.
- /*package*/ static synchronized Handler getHandler() {
- if (thread == null) {
- startThread(null);
- }
-
- while (handler == null) {
- try {
- GeckoBackgroundThread.class.wait();
- } catch (final InterruptedException e) {
- }
- }
- return handler;
- }
-
- /*package*/ static synchronized void post(final Runnable runnable) {
- if (thread == null) {
- startThread(runnable);
- return;
- }
- getHandler().post(runnable);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoEventListener.java
+++ /dev/null
@@ -1,14 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-@RobocopTarget
-public interface GeckoEventListener {
- void handleMessage(String event, JSONObject message);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoJarReader.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/* 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.util;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.util.Log;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.mozglue.GeckoLoader;
-import org.mozilla.gecko.mozglue.NativeZip;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Stack;
-
-/* Reads out of a multiple level deep jar file such as
- * jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
- */
-public final class GeckoJarReader {
- private static final String LOGTAG = "GeckoJarReader";
-
- private GeckoJarReader() {}
-
- public static Bitmap getBitmap(Context context, Resources resources, String url) {
- BitmapDrawable drawable = getBitmapDrawable(context, resources, url);
- return (drawable != null) ? drawable.getBitmap() : null;
- }
-
- public static BitmapDrawable getBitmapDrawable(Context context, Resources resources,
- String url) {
- Stack<String> jarUrls = parseUrl(url);
- InputStream inputStream = null;
- BitmapDrawable bitmap = null;
-
- NativeZip zip = null;
- try {
- // Load the initial jar file as a zip
- zip = getZipFile(context, jarUrls.pop());
- inputStream = getStream(zip, jarUrls, url);
- if (inputStream != null) {
- bitmap = new BitmapDrawable(resources, inputStream);
- // BitmapDrawable created from a stream does not set the correct target density from resources.
- // In fact it discards the resources https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/graphics/java/android/graphics/drawable/BitmapDrawable.java#191
- bitmap.setTargetDensity(resources.getDisplayMetrics());
- }
- } catch (IOException | URISyntaxException ex) {
- Log.e(LOGTAG, "Exception ", ex);
- } finally {
- if (inputStream != null) {
- try {
- inputStream.close();
- } catch (IOException ex) {
- Log.e(LOGTAG, "Error closing stream", ex);
- }
- }
- if (zip != null) {
- zip.close();
- }
- }
-
- return bitmap;
- }
-
- public static String getText(Context context, String url) {
- Stack<String> jarUrls = parseUrl(url);
-
- NativeZip zip = null;
- BufferedReader reader = null;
- String text = null;
- try {
- zip = getZipFile(context, jarUrls.pop());
- InputStream input = getStream(zip, jarUrls, url);
- if (input != null) {
- reader = new BufferedReader(new InputStreamReader(input));
- text = reader.readLine();
- }
- } catch (IOException | URISyntaxException ex) {
- Log.e(LOGTAG, "Exception ", ex);
- } finally {
- if (reader != null) {
- try {
- reader.close();
- } catch (IOException ex) {
- Log.e(LOGTAG, "Error closing reader", ex);
- }
- }
- if (zip != null) {
- zip.close();
- }
- }
-
- return text;
- }
-
- private static NativeZip getZipFile(Context context, String url)
- throws IOException, URISyntaxException {
- URI fileUrl = new URI(url);
- GeckoLoader.loadMozGlue(context);
- return new NativeZip(fileUrl.getPath());
- }
-
- @RobocopTarget
- /**
- * Extract a (possibly nested) file from an archive and write it to a temporary file.
- *
- * @param context Android context.
- * @param url to open. Can include jar: to "reach into" nested archives.
- * @param dir to write temporary file to.
- * @return a <code>File</code>, if one could be written; otherwise null.
- * @throws IOException if an error occured.
- */
- public static File extractStream(Context context, String url, File dir, String suffix) throws IOException {
- InputStream input = null;
- try {
- try {
- final URI fileURI = new URI(url);
- // We don't check the scheme because we want to catch bare files, not just file:// URIs.
- // If we let bare files through, we'd try to open them as ZIP files later -- and crash in native code.
- if (fileURI != null && fileURI.getPath() != null) {
- final File inputFile = new File(fileURI.getPath());
- if (inputFile != null && inputFile.exists()) {
- input = new FileInputStream(inputFile);
- }
- }
- } catch (URISyntaxException e) {
- // Not a file:// URI.
- }
- if (input == null) {
- // No luck with file:// URI; maybe some other URI?
- input = getStream(context, url);
- }
- if (input == null) {
- // Not found!
- return null;
- }
-
- // n.b.: createTempFile does not in fact delete the file.
- final File file = File.createTempFile("extractStream", suffix, dir);
- OutputStream output = null;
- try {
- output = new FileOutputStream(file);
- byte[] buf = new byte[8192];
- int len;
- while ((len = input.read(buf)) >= 0) {
- output.write(buf, 0, len);
- }
- return file;
- } finally {
- if (output != null) {
- output.close();
- }
- }
- } finally {
- if (input != null) {
- try {
- input.close();
- } catch (IOException e) {
- Log.w(LOGTAG, "Got exception closing stream; ignoring.", e);
- }
- }
- }
- }
-
- @RobocopTarget
- public static InputStream getStream(Context context, String url) {
- Stack<String> jarUrls = parseUrl(url);
- try {
- NativeZip zip = getZipFile(context, jarUrls.pop());
- return getStream(zip, jarUrls, url);
- } catch (Exception ex) {
- // Some JNI code throws IllegalArgumentException on a bad file name;
- // swallow the error and return null. We could also see legitimate
- // IOExceptions here.
- Log.e(LOGTAG, "Exception getting input stream from jar URL: " + url, ex);
- return null;
- }
- }
-
- private static InputStream getStream(NativeZip zip, Stack<String> jarUrls, String origUrl) {
- InputStream inputStream = null;
-
- // loop through children jar files until we reach the innermost one
- while (!jarUrls.empty()) {
- String fileName = jarUrls.pop();
-
- if (inputStream != null) {
- // intermediate NativeZips and InputStreams will be garbage collected.
- try {
- zip = new NativeZip(inputStream);
- } catch (IllegalArgumentException e) {
- String description = "!!! BUG 849589 !!! origUrl=" + origUrl;
- Log.e(LOGTAG, description, e);
- throw new IllegalArgumentException(description);
- }
- }
-
- inputStream = zip.getInputStream(fileName);
- if (inputStream == null) {
- Log.d(LOGTAG, "No Entry for " + fileName);
- return null;
- }
- }
-
- return inputStream;
- }
-
- /* Returns a stack of strings breaking the url up into pieces. Each piece
- * is assumed to point to a jar file except for the final one. Callers should
- * pass in the url to parse, and null for the parent parameter (used for recursion)
- * For example, jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
- * will return:
- * file:///data/app/org.mozilla.fennec.apk
- * omni.ja
- * chrome/chrome/content/branding/favicon32.png
- */
- private static Stack<String> parseUrl(String url) {
- return parseUrl(url, null);
- }
-
- private static Stack<String> parseUrl(String url, Stack<String> results) {
- if (results == null) {
- results = new Stack<String>();
- }
-
- if (url.startsWith("jar:")) {
- int jarEnd = url.lastIndexOf("!");
- String subStr = url.substring(4, jarEnd);
- results.push(url.substring(jarEnd + 2)); // remove the !/ characters
- return parseUrl(subStr, results);
- } else {
- results.push(url);
- return results;
- }
- }
-
- public static String getJarURL(Context context, String pathInsideJAR) {
- // We need to encode the package resource path, because it might contain illegal characters. For example:
- // /mnt/asec2/[2]org.mozilla.fennec-1/pkg.apk
- // The round-trip through a URI does this for us.
- final String resourcePath = context.getPackageResourcePath();
- return computeJarURI(resourcePath, pathInsideJAR);
- }
-
- /**
- * Encodes its resource path correctly.
- */
- @RobocopTarget
- public static String computeJarURI(String resourcePath, String pathInsideJAR) {
- final String resURI = new File(resourcePath).toURI().toString();
-
- // TODO: do we need to encode the file path, too?
- return "jar:jar:" + resURI + "!/" + AppConstants.OMNIJAR_NAME + "!/" + pathInsideJAR;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoRequest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/* 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.util;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import android.util.Log;
-
-public abstract class GeckoRequest {
- private static final String LOGTAG = "GeckoRequest";
- private static final AtomicInteger currentId = new AtomicInteger(0);
-
- private final int id = currentId.getAndIncrement();
- private final String name;
- private final String data;
-
- /**
- * Creates a request that can be dispatched using
- * {@link GeckoAppShell#sendRequestToGecko(GeckoRequest)}.
- *
- * @param name The name of the event associated with this request, which must have a
- * Gecko-side listener registered to respond to this request.
- * @param data Data to send with this request, which can be any object serializable by
- * {@link JSONObject#put(String, Object)}.
- */
- @RobocopTarget
- public GeckoRequest(String name, Object data) {
- this.name = name;
- final JSONObject message = new JSONObject();
- try {
- message.put("id", id);
- message.put("data", data);
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON error", e);
- }
- this.data = message.toString();
- }
-
- /**
- * Gets the ID for this request.
- *
- * @return The request ID
- */
- public int getId() {
- return id;
- }
-
- /**
- * Gets the event name associated with this request.
- *
- * @return The name of the event sent to Gecko
- */
- public String getName() {
- return name;
- }
-
- /**
- * Gets the stringified data associated with this request.
- *
- * @return The data being sent with the request
- */
- public String getData() {
- return data;
- }
-
- /**
- * Callback executed when the request succeeds.
- *
- * @param nativeJSObject The response data from Gecko
- */
- @RobocopTarget
- public abstract void onResponse(NativeJSObject nativeJSObject);
-
- /**
- * Callback executed when the request fails.
- *
- * By default, an exception is thrown. This should be overridden if the
- * GeckoRequest is able to recover from the error.
- *
- * @throws RuntimeException
- */
- @RobocopTarget
- public void onError(NativeJSObject error) {
- final String message = error.optString("message", "<no message>");
- final String stack = error.optString("stack", "<no stack>");
- throw new RuntimeException("Unhandled error for GeckoRequest " + name + ": " + message + "\nJS stack:\n" + stack);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * * This Source Code Form is subject to the terms of the Mozilla Public
- * * License, v. 2.0. If a copy of the MPL was not distributed with this
- * * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-
-package org.mozilla.gecko.util;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecInfo.CodecCapabilities;
-import android.media.MediaCodecList;
-import android.util.Log;
-
-public final class HardwareCodecCapabilityUtils {
- private static final String LOGTAG = "GeckoHardwareCodecCapabilityUtils";
-
- // List of supported HW VP8 encoders.
- private static final String[] supportedVp8HwEncCodecPrefixes =
- {"OMX.qcom.", "OMX.Intel." };
- // List of supported HW VP8 decoders.
- private static final String[] supportedVp8HwDecCodecPrefixes =
- {"OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "OMX.Intel." };
- private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
- // NV12 color format supported by QCOM codec, but not declared in MediaCodec -
- // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
- private static final int
- COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
- // Allowable color formats supported by codec - in order of preference.
- private static final int[] supportedColorList = {
- CodecCapabilities.COLOR_FormatYUV420Planar,
- CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
- CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
- COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
- };
-
- @WrapForJNI(allowMultithread = true, stubName = "FindDecoderCodecInfoForMimeType")
- public static boolean findDecoderCodecInfoForMimeType(String aMimeType) {
- for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
- MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
- if (info.isEncoder()) {
- continue;
- }
- for (String mimeType : info.getSupportedTypes()) {
- if (mimeType.equals(aMimeType)) {
- return true;
- }
- }
- }
- return false;
- }
-
- public static boolean getHWEncoderCapability() {
- if (Versions.feature20Plus) {
- for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
- MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
- if (!info.isEncoder()) {
- continue;
- }
- String name = null;
- for (String mimeType : info.getSupportedTypes()) {
- if (mimeType.equals(VP8_MIME_TYPE)) {
- name = info.getName();
- break;
- }
- }
- if (name == null) {
- continue; // No HW support in this codec; try the next one.
- }
- Log.e(LOGTAG, "Found candidate encoder " + name);
-
- // Check if this is supported encoder.
- boolean supportedCodec = false;
- for (String codecPrefix : supportedVp8HwEncCodecPrefixes) {
- if (name.startsWith(codecPrefix)) {
- supportedCodec = true;
- break;
- }
- }
- if (!supportedCodec) {
- continue;
- }
-
- // Check if codec supports either yuv420 or nv12.
- CodecCapabilities capabilities =
- info.getCapabilitiesForType(VP8_MIME_TYPE);
- for (int colorFormat : capabilities.colorFormats) {
- Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
- }
- for (int supportedColorFormat : supportedColorList) {
- for (int codecColorFormat : capabilities.colorFormats) {
- if (codecColorFormat == supportedColorFormat) {
- // Found supported HW Encoder.
- Log.e(LOGTAG, "Found target encoder " + name +
- ". Color: 0x" + Integer.toHexString(codecColorFormat));
- return true;
- }
- }
- }
- }
- }
- // No HW encoder.
- return false;
- }
-
- public static boolean getHWDecoderCapability() {
- if (Versions.feature20Plus) {
- for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
- MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
- if (info.isEncoder()) {
- continue;
- }
- String name = null;
- for (String mimeType : info.getSupportedTypes()) {
- if (mimeType.equals(VP8_MIME_TYPE)) {
- name = info.getName();
- break;
- }
- }
- if (name == null) {
- continue; // No HW support in this codec; try the next one.
- }
- Log.e(LOGTAG, "Found candidate decoder " + name);
-
- // Check if this is supported decoder.
- boolean supportedCodec = false;
- for (String codecPrefix : supportedVp8HwDecCodecPrefixes) {
- if (name.startsWith(codecPrefix)) {
- supportedCodec = true;
- break;
- }
- }
- if (!supportedCodec) {
- continue;
- }
-
- // Check if codec supports either yuv420 or nv12.
- CodecCapabilities capabilities =
- info.getCapabilitiesForType(VP8_MIME_TYPE);
- for (int colorFormat : capabilities.colorFormats) {
- Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
- }
- for (int supportedColorFormat : supportedColorList) {
- for (int codecColorFormat : capabilities.colorFormats) {
- if (codecColorFormat == supportedColorFormat) {
- // Found supported HW decoder.
- Log.e(LOGTAG, "Found target decoder " + name +
- ". Color: 0x" + Integer.toHexString(codecColorFormat));
- return true;
- }
- }
- }
- }
- }
- return false; // No HW decoder.
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.SysInfo;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.os.Build;
-import android.util.Log;
-import android.view.ViewConfiguration;
-
-public final class HardwareUtils {
- private static final String LOGTAG = "GeckoHardwareUtils";
-
- private static final boolean IS_AMAZON_DEVICE = Build.MANUFACTURER.equalsIgnoreCase("Amazon");
- public static final boolean IS_KINDLE_DEVICE = IS_AMAZON_DEVICE &&
- (Build.MODEL.equals("Kindle Fire") ||
- Build.MODEL.startsWith("KF"));
-
- private static volatile boolean sInited;
-
- // These are all set once, during init.
- private static volatile boolean sIsLargeTablet;
- private static volatile boolean sIsSmallTablet;
- private static volatile boolean sIsTelevision;
-
- private HardwareUtils() {
- }
-
- public static void init(Context context) {
- if (sInited) {
- // This is unavoidable, given that HardwareUtils is called from background services.
- Log.d(LOGTAG, "HardwareUtils already inited.");
- return;
- }
-
- // Pre-populate common flags from the context.
- final int screenLayoutSize = context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
- if (Build.VERSION.SDK_INT >= 11) {
- if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
- sIsLargeTablet = true;
- } else if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE) {
- sIsSmallTablet = true;
- }
- if (Build.VERSION.SDK_INT >= 16) {
- if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
- sIsTelevision = true;
- }
- }
- }
-
- sInited = true;
- }
-
- public static boolean isTablet() {
- return sIsLargeTablet || sIsSmallTablet;
- }
-
- public static boolean isLargeTablet() {
- return sIsLargeTablet;
- }
-
- public static boolean isSmallTablet() {
- return sIsSmallTablet;
- }
-
- public static boolean isTelevision() {
- return sIsTelevision;
- }
-
- public static int getMemSize() {
- return SysInfo.getMemSize();
- }
-
- /**
- * @return false if the current system is not supported (e.g. APK/system ABI mismatch).
- */
- public static boolean isSupportedSystem() {
- if (Build.VERSION.SDK_INT < AppConstants.Versions.MIN_SDK_VERSION ||
- Build.VERSION.SDK_INT > AppConstants.Versions.MAX_SDK_VERSION) {
- return false;
- }
-
- // See http://developer.android.com/ndk/guides/abis.html
- boolean isSystemARM = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("arm");
- boolean isSystemX86 = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("x86");
-
- boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("arm");
- boolean isAppX86 = AppConstants.ANDROID_CPU_ARCH.startsWith("x86");
-
- // Only reject known incompatible ABIs. Better safe than sorry.
- if ((isSystemX86 && isAppARM) || (isSystemARM && isAppX86)) {
- return false;
- }
-
- if ((isSystemX86 && isAppX86) || (isSystemARM && isAppARM)) {
- return true;
- }
-
- Log.w(LOGTAG, "Unknown app/system ABI combination: " + AppConstants.MOZ_APP_ABI + " / " + Build.CPU_ABI);
- return true;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/INIParser.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/* 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.util;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.util.Enumeration;
-import java.util.Hashtable;
-
-public final class INIParser extends INISection {
- // default file to read and write to
- private final File mFile;
-
- // List of sections in the current iniFile. null if the file has not been parsed yet
- private Hashtable<String, INISection> mSections;
-
- // create a parser. The file will not be read until you attempt to
- // access sections or properties inside it. At that point its read synchronously
- public INIParser(File iniFile) {
- super("");
- mFile = iniFile;
- }
-
- // write ini data to the default file. Will overwrite anything current inside
- public void write() {
- writeTo(mFile);
- }
-
- // write to the specified file. Will overwrite anything current inside
- public void writeTo(File f) {
- if (f == null)
- return;
-
- FileWriter outputStream = null;
- try {
- outputStream = new FileWriter(f);
- } catch (IOException e1) {
- e1.printStackTrace();
- }
-
- BufferedWriter writer = new BufferedWriter(outputStream);
- try {
- write(writer);
- writer.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void write(BufferedWriter writer) throws IOException {
- super.write(writer);
-
- if (mSections != null) {
- for (Enumeration<INISection> e = mSections.elements(); e.hasMoreElements();) {
- INISection section = e.nextElement();
- section.write(writer);
- writer.newLine();
- }
- }
- }
-
- // return all of the sections inside this file
- public Hashtable<String, INISection> getSections() {
- if (mSections == null) {
- try {
- parse();
- } catch (IOException e) {
- debug("Error parsing: " + e);
- }
- }
- return mSections;
- }
-
- // parse the default file
- @Override
- protected void parse() throws IOException {
- super.parse();
- parse(mFile);
- }
-
- // parse a passed in file
- private void parse(File f) throws IOException {
- // Set up internal data members
- mSections = new Hashtable<String, INISection>();
-
- if (f == null || !f.exists())
- return;
-
- FileReader inputStream = null;
- try {
- inputStream = new FileReader(f);
- } catch (FileNotFoundException e1) {
- // If the file doesn't exist. Just return;
- return;
- }
-
- BufferedReader buf = new BufferedReader(inputStream);
- String line = null; // current line of text we are parsing
- INISection currentSection = null; // section we are currently parsing
-
- while ((line = buf.readLine()) != null) {
-
- if (line != null)
- line = line.trim();
-
- // blank line or a comment. ignore it
- if (line == null || line.length() == 0 || line.charAt(0) == ';') {
- debug("Ignore line: " + line);
- } else if (line.charAt(0) == '[') {
- debug("Parse as section: " + line);
- currentSection = new INISection(line.substring(1, line.length() - 1));
- mSections.put(currentSection.getName(), currentSection);
- } else {
- debug("Parse as property: " + line);
-
- String[] pieces = line.split("=");
- if (pieces.length != 2)
- continue;
-
- String key = pieces[0].trim();
- String value = pieces[1].trim();
- if (currentSection != null) {
- currentSection.setProperty(key, value);
- } else {
- mProperties.put(key, value);
- }
- }
- }
- buf.close();
- }
-
- // add a section to the file
- public void addSection(INISection sect) {
- // ensure that we have parsed the file
- getSections();
- mSections.put(sect.getName(), sect);
- }
-
- // get a section from the file. will return null if the section doesn't exist
- public INISection getSection(String key) {
- // ensure that we have parsed the file
- getSections();
- return mSections.get(key);
- }
-
- // remove an entire section from the file
- public void removeSection(String name) {
- // ensure that we have parsed the file
- getSections();
- mSections.remove(name);
- }
-
- // rename a section; nuking any previous section with the new
- // name in the process
- public void renameSection(String oldName, String newName) {
- // ensure that we have parsed the file
- getSections();
-
- mSections.remove(newName);
- INISection section = mSections.get(oldName);
- if (section == null)
- return;
-
- section.setName(newName);
- mSections.remove(oldName);
- mSections.put(newName, section);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/INISection.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/* 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.util;
-
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.util.Enumeration;
-import java.util.Hashtable;
-
-public class INISection {
- private static final String LOGTAG = "INIParser";
-
- // default file to read and write to
- private String mName;
- public String getName() { return mName; }
- public void setName(String name) { mName = name; }
-
- // show or hide debug logging
- private boolean mDebug;
-
- // Global properties that aren't inside a section in the file
- protected Hashtable<String, Object> mProperties;
-
- // create a parser. The file will not be read until you attempt to
- // access sections or properties inside it. At that point its read synchronously
- public INISection(String name) {
- mName = name;
- }
-
- // log a debug string to the console
- protected void debug(String msg) {
- if (mDebug) {
- Log.i(LOGTAG, msg);
- }
- }
-
- // get a global property out of the hash table. will return null if the property doesn't exist
- public Object getProperty(String key) {
- getProperties(); // ensure that we have parsed the file
- return mProperties.get(key);
- }
-
- // get a global property out of the hash table. will return null if the property doesn't exist
- public int getIntProperty(String key) {
- Object val = getProperty(key);
- if (val == null)
- return -1;
-
- return Integer.parseInt(val.toString());
- }
-
- // get a global property out of the hash table. will return null if the property doesn't exist
- public String getStringProperty(String key) {
- Object val = getProperty(key);
- if (val == null)
- return null;
-
- return val.toString();
- }
-
- // get a hashtable of all the global properties in this file
- public Hashtable<String, Object> getProperties() {
- if (mProperties == null) {
- try {
- parse();
- } catch (IOException e) {
- debug("Error parsing: " + e);
- }
- }
- return mProperties;
- }
-
- // do nothing for generic sections
- protected void parse() throws IOException {
- mProperties = new Hashtable<String, Object>();
- }
-
- // set a property. Will erase the property if value = null
- public void setProperty(String key, Object value) {
- getProperties(); // ensure that we have parsed the file
- if (value == null)
- removeProperty(key);
- else
- mProperties.put(key.trim(), value);
- }
-
- // remove a property
- public void removeProperty(String name) {
- // ensure that we have parsed the file
- getProperties();
- mProperties.remove(name);
- }
-
- public void write(BufferedWriter writer) throws IOException {
- if (!TextUtils.isEmpty(mName)) {
- writer.write("[" + mName + "]");
- writer.newLine();
- }
-
- if (mProperties != null) {
- for (Enumeration<String> e = mProperties.keys(); e.hasMoreElements();) {
- String key = e.nextElement();
- writeProperty(writer, key, mProperties.get(key));
- }
- }
- writer.newLine();
- }
-
- // Helper function to write out a property
- private void writeProperty(BufferedWriter writer, String key, Object value) {
- try {
- writer.write(key + "=" + value);
- writer.newLine();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/IOUtils.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/* -*- 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.util;
-
-import android.util.Log;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Static helper class containing useful methods for manipulating IO objects.
- */
-public class IOUtils {
- private static final String LOGTAG = "GeckoIOUtils";
-
- /**
- * Represents the result of consuming an input stream, holding the returned data as well
- * as the length of the data returned.
- * The byte[] is not guaranteed to be trimmed to the size of the data acquired from the stream:
- * hence the need for the length field. This strategy avoids the need to copy the data into a
- * trimmed buffer after consumption.
- */
- public static class ConsumedInputStream {
- public final int consumedLength;
- // Only reassigned in getTruncatedData.
- private byte[] consumedData;
-
- public ConsumedInputStream(int consumedLength, byte[] consumedData) {
- this.consumedLength = consumedLength;
- this.consumedData = consumedData;
- }
-
- /**
- * Get the data trimmed to the length of the actual payload read, caching the result.
- */
- public byte[] getTruncatedData() {
- if (consumedData.length == consumedLength) {
- return consumedData;
- }
-
- consumedData = truncateBytes(consumedData, consumedLength);
- return consumedData;
- }
-
- public byte[] getData() {
- return consumedData;
- }
- }
-
- /**
- * Fully read an InputStream into a byte array.
- * @param iStream the InputStream to consume.
- * @param bufferSize The initial size of the buffer to allocate. It will be grown as
- * needed, but if the caller knows something about the InputStream then
- * passing a good value here can improve performance.
- */
- public static ConsumedInputStream readFully(InputStream iStream, int bufferSize) {
- // Allocate a buffer to hold the raw data downloaded.
- byte[] buffer = new byte[bufferSize];
-
- // The offset of the start of the buffer's free space.
- int bPointer = 0;
-
- // The quantity of bytes the last call to read yielded.
- int lastRead = 0;
- try {
- // Fully read the data into the buffer.
- while (lastRead != -1) {
- // Read as many bytes as are currently available into the buffer.
- lastRead = iStream.read(buffer, bPointer, buffer.length - bPointer);
- bPointer += lastRead;
-
- // If buffer has overflowed, double its size and carry on.
- if (bPointer == buffer.length) {
- bufferSize *= 2;
- byte[] newBuffer = new byte[bufferSize];
-
- // Copy the contents of the old buffer into the new buffer.
- System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
- buffer = newBuffer;
- }
- }
-
- return new ConsumedInputStream(bPointer + 1, buffer);
- } catch (IOException e) {
- Log.e(LOGTAG, "Error consuming input stream.", e);
- } finally {
- try {
- iStream.close();
- } catch (IOException e) {
- Log.e(LOGTAG, "Error closing input stream.", e);
- }
- }
-
- return null;
- }
-
- /**
- * Truncate a given byte[] to a given length. Returns a new byte[] with the first length many
- * bytes of the input.
- */
- public static byte[] truncateBytes(byte[] bytes, int length) {
- byte[] newBytes = new byte[length];
- System.arraycopy(bytes, 0, newBytes, 0, length);
-
- return newBytes;
- }
-
- public static void safeStreamClose(Closeable stream) {
- try {
- if (stream != null)
- stream.close();
- } catch (IOException e) { }
- }
-
- public static void copy(InputStream in, OutputStream out) throws IOException {
- byte[] buffer = new byte[4096];
- int len;
-
- while ((len = in.read(buffer)) != -1) {
- out.write(buffer, 0, len);
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/InputOptionsUtils.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/* -*- 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.util;
-
-import android.content.Context;
-import android.content.Intent;
-import android.speech.RecognizerIntent;
-
-public class InputOptionsUtils {
- public static boolean supportsVoiceRecognizer(Context context, String prompt) {
- final Intent intent = createVoiceRecognizerIntent(prompt);
- return intent.resolveActivity(context.getPackageManager()) != null;
- }
-
- public static Intent createVoiceRecognizerIntent(String prompt) {
- final Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
- intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
- intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
- intent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
- return intent;
- }
-
- public static boolean supportsIntent(Intent intent, Context context) {
- return intent.resolveActivity(context.getPackageManager()) != null;
- }
-
- public static boolean supportsQrCodeReader(Context context) {
- final Intent intent = createQRCodeReaderIntent();
- return supportsIntent(intent, context);
- }
-
- public static Intent createQRCodeReaderIntent() {
- // Bug 602818 enables QR code input if you have the particular app below installed in your device
- final String appPackage = "com.google.zxing.client.android";
-
- Intent intent = new Intent(appPackage + ".SCAN");
- intent.setPackage(appPackage);
- intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- return intent;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/IntentUtils.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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.util;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import org.mozilla.gecko.mozglue.SafeIntent;
-
-import java.util.HashMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Utilities for Intents.
- */
-public class IntentUtils {
- public static final String ENV_VAR_IN_AUTOMATION = "MOZ_IN_AUTOMATION";
-
- private static final String ENV_VAR_REGEX = "(.+)=(.*)";
-
- private IntentUtils() {}
-
- /**
- * Returns a list of environment variables and their values. These are parsed from an Intent extra
- * with the key -> value format:
- * env# -> ENV_VAR=VALUE
- *
- * # in env# is expected to be increasing from 0.
- *
- * @return A Map of environment variable name to value, e.g. ENV_VAR -> VALUE
- */
- public static HashMap<String, String> getEnvVarMap(@NonNull final Intent unsafeIntent) {
- // Optimization: get matcher for re-use. Pattern.matcher creates a new object every time so it'd be great
- // to avoid the unnecessary allocation, particularly because we expect to be called on the startup path.
- final Pattern envVarPattern = Pattern.compile(ENV_VAR_REGEX);
- final Matcher matcher = envVarPattern.matcher(""); // argument does not matter here.
-
- // This is expected to be an external intent so we should use SafeIntent to prevent crashing.
- final SafeIntent safeIntent = new SafeIntent(unsafeIntent);
- final HashMap<String, String> out = new HashMap<>();
- int i = 0;
- while (true) {
- final String envKey = "env" + i;
- i += 1;
- if (!unsafeIntent.hasExtra(envKey)) {
- break;
- }
-
- maybeAddEnvVarToEnvVarMap(out, safeIntent, envKey, matcher);
- }
- return out;
- }
-
- /**
- * @param envVarMap the map to add the env var to
- * @param intent the intent from which to extract the env var
- * @param envKey the key at which the env var resides
- * @param envVarMatcher a matcher initialized with the env var pattern to extract
- */
- private static void maybeAddEnvVarToEnvVarMap(@NonNull final HashMap<String, String> envVarMap,
- @NonNull final SafeIntent intent, @NonNull final String envKey, @NonNull final Matcher envVarMatcher) {
- final String envValue = intent.getStringExtra(envKey);
- if (envValue == null) {
- return; // nothing to do here!
- }
-
- envVarMatcher.reset(envValue);
- if (envVarMatcher.matches()) {
- final String envVarName = envVarMatcher.group(1);
- final String envVarValue = envVarMatcher.group(2);
- envVarMap.put(envVarName, envVarValue);
- }
- }
-
- public static Bundle getBundleExtraSafe(final Intent intent, final String name) {
- return new SafeIntent(intent).getBundleExtra(name);
- }
-
- public static String getStringExtraSafe(final Intent intent, final String name) {
- return new SafeIntent(intent).getStringExtra(name);
- }
-
- public static boolean getBooleanExtraSafe(final Intent intent, final String name, final boolean defaultValue) {
- return new SafeIntent(intent).getBooleanExtra(name, defaultValue);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/JSONUtils.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/* 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.util;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.os.Bundle;
-import android.util.Log;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.UUID;
-
-public final class JSONUtils {
- private static final String LOGTAG = "GeckoJSONUtils";
-
- private JSONUtils() {}
-
- public static UUID getUUID(String name, JSONObject json) {
- String uuid = json.optString(name, null);
- return (uuid != null) ? UUID.fromString(uuid) : null;
- }
-
- public static void putUUID(String name, UUID uuid, JSONObject json) {
- String uuidString = uuid.toString();
- try {
- json.put(name, uuidString);
- } catch (JSONException e) {
- throw new IllegalArgumentException(name + "=" + uuidString, e);
- }
- }
-
- public static JSONObject bundleToJSON(Bundle bundle) {
- if (bundle == null || bundle.isEmpty()) {
- return null;
- }
-
- JSONObject json = new JSONObject();
- for (String key : bundle.keySet()) {
- try {
- json.put(key, bundle.get(key));
- } catch (JSONException e) {
- Log.w(LOGTAG, "Error building JSON response.", e);
- }
- }
-
- return json;
- }
-
- // Handles conversions between a JSONArray and a Set<String>
- public static Set<String> parseStringSet(JSONArray json) {
- final Set<String> ret = new HashSet<String>();
-
- for (int i = 0; i < json.length(); i++) {
- try {
- ret.add(json.getString(i));
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Error parsing json", ex);
- }
- }
-
- return ret;
- }
-
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/MenuUtils.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.view.Menu;
-import android.view.MenuItem;
-
-public class MenuUtils {
- /*
- * This method looks for a menuitem and sets it's visible state, if
- * it exists.
- */
- public static void safeSetVisible(Menu menu, int id, boolean visible) {
- MenuItem item = menu.findItem(id);
- if (item != null) {
- item.setVisible(visible);
- }
- }
-
- /*
- * This method looks for a menuitem and sets it's enabled state, if
- * it exists.
- */
- public static void safeSetEnabled(Menu menu, int id, boolean enabled) {
- MenuItem item = menu.findItem(id);
- if (item != null) {
- item.setEnabled(enabled);
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NativeEventListener.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-@RobocopTarget
-public interface NativeEventListener {
- /**
- * Handles a message sent from Gecko.
- *
- * @param event The name of the event being sent.
- * @param message The message data.
- * @param callback The callback interface for this message. A callback is provided only if the
- * originating Messaging.sendRequest call included a callback argument; otherwise,
- * callback will be null. All listeners for a given event are given the same
- * callback object, and exactly one listener must handle the callback.
- */
- void handleMessage(String event, NativeJSObject message, EventCallback callback);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NativeJSContainer.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/* -*- 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.util;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-/**
- * NativeJSContainer is a wrapper around the SpiderMonkey JSAPI to make it possible to
- * access Javascript objects in Java.
- *
- * A container must only be used on the thread it is attached to. To use it on another
- * thread, call {@link #clone()} to make a copy, and use the copy on the other thread.
- * When a copy is first used, it becomes attached to the thread using it.
- */
-@WrapForJNI
-public final class NativeJSContainer extends NativeJSObject
-{
- private NativeJSContainer() {
- }
-
- /**
- * Make a copy of this container for use by another thread. When the copy is first used,
- * it becomes attached to the thread using it.
- */
- @Override
- public native NativeJSContainer clone();
-
- /**
- * Dispose all associated native objects. Subsequent use of any objects derived from
- * this container will throw a NullPointerException.
- */
- @Override
- public native void disposeNative();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NativeJSObject.java
+++ /dev/null
@@ -1,533 +0,0 @@
-/* -*- 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.util;
-
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.mozglue.JNIObject;
-
-import android.os.Bundle;
-
-/**
- * NativeJSObject is a wrapper around the SpiderMonkey JSAPI to make it possible to
- * access Javascript objects in Java.
- */
-@WrapForJNI
-public class NativeJSObject extends JNIObject
-{
- @SuppressWarnings("serial")
- @JNITarget
- public static final class InvalidPropertyException extends RuntimeException {
- public InvalidPropertyException(final String msg) {
- super(msg);
- }
- }
-
- protected NativeJSObject() {
- }
-
- @Override
- protected void disposeNative() {
- // NativeJSObject is disposed as part of NativeJSContainer disposal.
- }
-
- /**
- * Returns the value of a boolean property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native boolean getBoolean(String name);
-
- /**
- * Returns the value of a boolean property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native boolean optBoolean(String name, boolean fallback);
-
- /**
- * Returns the value of a boolean array property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native boolean[] getBooleanArray(String name);
-
- /**
- * Returns the value of a boolean array property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native boolean[] optBooleanArray(String name, boolean[] fallback);
-
- /**
- * Returns the value of an object property as a Bundle.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native Bundle getBundle(String name);
-
- /**
- * Returns the value of an object property as a Bundle.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native Bundle optBundle(String name, Bundle fallback);
-
- /**
- * Returns the value of an object array property as a Bundle array.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native Bundle[] getBundleArray(String name);
-
- /**
- * Returns the value of an object array property as a Bundle array.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native Bundle[] optBundleArray(String name, Bundle[] fallback);
-
- /**
- * Returns the value of a double property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native double getDouble(String name);
-
- /**
- * Returns the value of a double property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native double optDouble(String name, double fallback);
-
- /**
- * Returns the value of a double array property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native double[] getDoubleArray(String name);
-
- /**
- * Returns the value of a double array property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native double[] optDoubleArray(String name, double[] fallback);
-
- /**
- * Returns the value of an int property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native int getInt(String name);
-
- /**
- * Returns the value of an int property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native int optInt(String name, int fallback);
-
- /**
- * Returns the value of an int array property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native int[] getIntArray(String name);
-
- /**
- * Returns the value of an int array property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native int[] optIntArray(String name, int[] fallback);
-
- /**
- * Returns the value of an object property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native NativeJSObject getObject(String name);
-
- /**
- * Returns the value of an object property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native NativeJSObject optObject(String name, NativeJSObject fallback);
-
- /**
- * Returns the value of an object array property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native NativeJSObject[] getObjectArray(String name);
-
- /**
- * Returns the value of an object array property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native NativeJSObject[] optObjectArray(String name, NativeJSObject[] fallback);
-
- /**
- * Returns the value of a string property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native String getString(String name);
-
- /**
- * Returns the value of a string property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native String optString(String name, String fallback);
-
- /**
- * Returns the value of a string array property.
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property does not exist or if its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native String[] getStringArray(String name);
-
- /**
- * Returns the value of a string array property.
- *
- * @param name
- * Property name
- * @param fallback
- * Value to return if property does not exist
- * @throws IllegalArgumentException
- * If name is null
- * @throws InvalidPropertyException
- * If the property exists and its type does not match the return type
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native String[] optStringArray(String name, String[] fallback);
-
- /**
- * Returns whether a property exists in this object
- *
- * @param name
- * Property name
- * @throws IllegalArgumentException
- * If name is null
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native boolean has(String name);
-
- /**
- * Returns the Bundle representation of this object.
- *
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- public native Bundle toBundle();
-
- /**
- * Returns the JSON representation of this object.
- *
- * @throws NullPointerException
- * If this JS object has been disposed
- * @throws IllegalThreadStateException
- * If not called on the thread this object is attached to
- * @throws UnsupportedOperationException
- * If an internal JSAPI call failed
- */
- @Override
- public native String toString();
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NetworkUtils.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.support.annotation.Nullable;
-import android.support.annotation.NonNull;
-import android.telephony.TelephonyManager;
-
-public class NetworkUtils {
- /*
- * Keep the below constants in sync with
- * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
- */
- public enum ConnectionSubType {
- CELL_2G("2g"),
- CELL_3G("3g"),
- CELL_4G("4g"),
- ETHERNET("ethernet"),
- WIFI("wifi"),
- WIMAX("wimax"),
- UNKNOWN("unknown");
-
- public final String value;
- ConnectionSubType(String value) {
- this.value = value;
- }
- }
-
- /*
- * Keep the below constants in sync with
- * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
- */
- public enum NetworkStatus {
- UP("up"),
- DOWN("down"),
- UNKNOWN("unknown");
-
- public final String value;
-
- NetworkStatus(String value) {
- this.value = value;
- }
- }
-
- // Connection Type defined in Network Information API v3.
- // See Bug 1270401 - current W3C Spec (Editor's Draft) is different, it also contains wimax, mixed, unknown.
- // W3C spec: http://w3c.github.io/netinfo/#the-connectiontype-enum
- public enum ConnectionType {
- CELLULAR(0),
- BLUETOOTH(1),
- ETHERNET(2),
- WIFI(3),
- OTHER(4),
- NONE(5);
-
- public final int value;
-
- ConnectionType(int value) {
- this.value = value;
- }
- }
-
- /**
- * Indicates whether network connectivity exists and it is possible to establish connections and pass data.
- */
- public static boolean isConnected(@NonNull Context context) {
- return isConnected((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
- }
-
- public static boolean isConnected(ConnectivityManager connectivityManager) {
- if (connectivityManager == null) {
- return false;
- }
-
- final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
- return networkInfo != null && networkInfo.isConnected();
- }
-
- /**
- * For mobile connections, maps particular connection subtype to a general 2G, 3G, 4G bucket.
- */
- public static ConnectionSubType getConnectionSubType(ConnectivityManager connectivityManager) {
- if (connectivityManager == null) {
- return ConnectionSubType.UNKNOWN;
- }
-
- final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
-
- if (networkInfo == null) {
- return ConnectionSubType.UNKNOWN;
- }
-
- switch (networkInfo.getType()) {
- case ConnectivityManager.TYPE_ETHERNET:
- return ConnectionSubType.ETHERNET;
- case ConnectivityManager.TYPE_MOBILE:
- return getGenericMobileSubtype(networkInfo.getSubtype());
- case ConnectivityManager.TYPE_WIMAX:
- return ConnectionSubType.WIMAX;
- case ConnectivityManager.TYPE_WIFI:
- return ConnectionSubType.WIFI;
- default:
- return ConnectionSubType.UNKNOWN;
- }
- }
-
- public static ConnectionType getConnectionType(ConnectivityManager connectivityManager) {
- if (connectivityManager == null) {
- return ConnectionType.NONE;
- }
-
- final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
- if (networkInfo == null) {
- return ConnectionType.NONE;
- }
-
- switch (networkInfo.getType()) {
- case ConnectivityManager.TYPE_BLUETOOTH:
- return ConnectionType.BLUETOOTH;
- case ConnectivityManager.TYPE_ETHERNET:
- return ConnectionType.ETHERNET;
- // Fallthrough, MOBILE and WIMAX both map to CELLULAR.
- case ConnectivityManager.TYPE_MOBILE:
- case ConnectivityManager.TYPE_WIMAX:
- return ConnectionType.CELLULAR;
- case ConnectivityManager.TYPE_WIFI:
- return ConnectionType.WIFI;
- default:
- return ConnectionType.OTHER;
- }
- }
-
- public static NetworkStatus getNetworkStatus(ConnectivityManager connectivityManager) {
- if (connectivityManager == null) {
- return NetworkStatus.UNKNOWN;
- }
-
- if (isConnected(connectivityManager)) {
- return NetworkStatus.UP;
- }
- return NetworkStatus.DOWN;
- }
-
- private static ConnectionSubType getGenericMobileSubtype(int subtype) {
- switch (subtype) {
- // 2G types: fallthrough 5x
- case TelephonyManager.NETWORK_TYPE_GPRS:
- case TelephonyManager.NETWORK_TYPE_EDGE:
- case TelephonyManager.NETWORK_TYPE_CDMA:
- case TelephonyManager.NETWORK_TYPE_1xRTT:
- case TelephonyManager.NETWORK_TYPE_IDEN:
- return ConnectionSubType.CELL_2G;
- // 3G types: fallthrough 9x
- case TelephonyManager.NETWORK_TYPE_UMTS:
- case TelephonyManager.NETWORK_TYPE_EVDO_0:
- case TelephonyManager.NETWORK_TYPE_EVDO_A:
- case TelephonyManager.NETWORK_TYPE_HSDPA:
- case TelephonyManager.NETWORK_TYPE_HSUPA:
- case TelephonyManager.NETWORK_TYPE_HSPA:
- case TelephonyManager.NETWORK_TYPE_EVDO_B:
- case TelephonyManager.NETWORK_TYPE_EHRPD:
- case TelephonyManager.NETWORK_TYPE_HSPAP:
- return ConnectionSubType.CELL_3G;
- // 4G - just one type!
- case TelephonyManager.NETWORK_TYPE_LTE:
- return ConnectionSubType.CELL_4G;
- default:
- return ConnectionSubType.UNKNOWN;
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/NonEvictingLruCache.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.util.LruCache;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * An LruCache that also supports a set of items that will never be evicted.
- *
- * Alas, LruCache is final, so we compose rather than inherit.
- */
-public class NonEvictingLruCache<K, V> {
- private final ConcurrentHashMap<K, V> permanent = new ConcurrentHashMap<K, V>();
- private final LruCache<K, V> evictable;
-
- public NonEvictingLruCache(final int evictableSize) {
- evictable = new LruCache<K, V>(evictableSize);
- }
-
- public V get(K key) {
- V val = permanent.get(key);
- if (val == null) {
- return evictable.get(key);
- }
- return val;
- }
-
- public void putWithoutEviction(K key, V value) {
- permanent.put(key, value);
- }
-
- public void put(K key, V value) {
- evictable.put(key, value);
- }
-
- public void evictAll() {
- evictable.evictAll();
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/PrefUtils.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.content.SharedPreferences;
-import android.util.Log;
-
-
-public class PrefUtils {
- private static final String LOGTAG = "GeckoPrefUtils";
-
- // Cross version compatible way to get a string set from a pref
- public static Set<String> getStringSet(final SharedPreferences prefs,
- final String key,
- final Set<String> defaultVal) {
- if (!prefs.contains(key)) {
- return defaultVal;
- }
-
- // If this is Android version >= 11, try to use a Set<String>.
- try {
- return prefs.getStringSet(key, new HashSet<String>());
- } catch (ClassCastException ex) {
- // A ClassCastException means we've upgraded from a pre-v11 Android to a new one
- final Set<String> val = getFromJSON(prefs, key);
- SharedPreferences.Editor edit = prefs.edit();
- putStringSet(edit, key, val).apply();
- return val;
- }
- }
-
- private static Set<String> getFromJSON(SharedPreferences prefs, String key) {
- try {
- final String val = prefs.getString(key, "[]");
- return JSONUtils.parseStringSet(new JSONArray(val));
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Unable to parse JSON", ex);
- }
-
- return new HashSet<String>();
- }
-
- /**
- * Cross version compatible way to save a set of strings.
- * <p>
- * This method <b>does not commit</b> any transaction. It is up to callers
- * to commit.
- *
- * @param editor to write to.
- * @param key to write.
- * @param vals comprising string set.
- * @return
- */
- public static SharedPreferences.Editor putStringSet(final SharedPreferences.Editor editor,
- final String key,
- final Set<String> vals) {
- editor.putStringSet(key, vals);
- return editor;
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ProxySelector.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.
- */
-
-// This code is based on AOSP /libcore/luni/src/main/java/java/net/ProxySelectorImpl.java
-
-package org.mozilla.gecko.util;
-
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.Proxy;
-import java.net.URI;
-import java.net.URLConnection;
-import java.util.List;
-
-public class ProxySelector {
- public static URLConnection openConnectionWithProxy(URI uri) throws IOException {
- java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
- Proxy proxy = Proxy.NO_PROXY;
- if (ps != null) {
- List<Proxy> proxies = ps.select(uri);
- if (proxies != null && !proxies.isEmpty()) {
- proxy = proxies.get(0);
- }
- }
-
- return uri.toURL().openConnection(proxy);
- }
-
- public ProxySelector() {
- }
-
- public Proxy select(String scheme, String host) {
- int port = -1;
- Proxy proxy = null;
- String nonProxyHostsKey = null;
- boolean httpProxyOkay = true;
- if ("http".equalsIgnoreCase(scheme)) {
- port = 80;
- nonProxyHostsKey = "http.nonProxyHosts";
- proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
- } else if ("https".equalsIgnoreCase(scheme)) {
- port = 443;
- nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
- proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
- } else if ("ftp".equalsIgnoreCase(scheme)) {
- port = 80; // not 21 as you might guess
- nonProxyHostsKey = "ftp.nonProxyHosts";
- proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
- } else if ("socket".equalsIgnoreCase(scheme)) {
- httpProxyOkay = false;
- } else {
- return Proxy.NO_PROXY;
- }
-
- if (nonProxyHostsKey != null
- && isNonProxyHost(host, System.getProperty(nonProxyHostsKey))) {
- return Proxy.NO_PROXY;
- }
-
- if (proxy != null) {
- return proxy;
- }
-
- if (httpProxyOkay) {
- proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
- if (proxy != null) {
- return proxy;
- }
- }
-
- proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
- if (proxy != null) {
- return proxy;
- }
-
- return Proxy.NO_PROXY;
- }
-
- /**
- * Returns the proxy identified by the {@code hostKey} system property, or
- * null.
- */
- @Nullable
- private Proxy lookupProxy(String hostKey, String portKey, Proxy.Type type, int defaultPort) {
- final String host = System.getProperty(hostKey);
- if (TextUtils.isEmpty(host)) {
- return null;
- }
-
- final int port = getSystemPropertyInt(portKey, defaultPort);
- if (port == -1) {
- // Port can be -1. See bug 1270529.
- return null;
- }
-
- return new Proxy(type, InetSocketAddress.createUnresolved(host, port));
- }
-
- private int getSystemPropertyInt(String key, int defaultValue) {
- String string = System.getProperty(key);
- if (string != null) {
- try {
- return Integer.parseInt(string);
- } catch (NumberFormatException ignored) {
- }
- }
- return defaultValue;
- }
-
- /**
- * Returns true if the {@code nonProxyHosts} system property pattern exists
- * and matches {@code host}.
- */
- private boolean isNonProxyHost(String host, String nonProxyHosts) {
- if (host == null || nonProxyHosts == null) {
- return false;
- }
-
- // construct pattern
- StringBuilder patternBuilder = new StringBuilder();
- for (int i = 0; i < nonProxyHosts.length(); i++) {
- char c = nonProxyHosts.charAt(i);
- switch (c) {
- case '.':
- patternBuilder.append("\\.");
- break;
- case '*':
- patternBuilder.append(".*");
- break;
- default:
- patternBuilder.append(c);
- }
- }
- // check whether the host is the nonProxyHosts.
- String pattern = patternBuilder.toString();
- return host.matches(pattern);
- }
-}
-
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/RawResource.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* 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.util;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.StringWriter;
-
-/**
- * {@code RawResource} provides API to load raw resources in different
- * forms. For now, we only load them as strings. We're using raw resources
- * as localizable 'assets' as opposed to a string that can be directly
- * translatable e.g. JSON file vs string.
- *
- * This is just a utility class to avoid code duplication for the different
- * cases where need to read such assets.
- */
-public final class RawResource {
- public static String getAsString(Context context, int id) throws IOException {
- InputStreamReader reader = null;
-
- try {
- final Resources res = context.getResources();
- final InputStream is = res.openRawResource(id);
- if (is == null) {
- return null;
- }
-
- reader = new InputStreamReader(is);
-
- final char[] buffer = new char[1024];
- final StringWriter s = new StringWriter();
-
- int n;
- while ((n = reader.read(buffer, 0, buffer.length)) != -1) {
- s.write(buffer, 0, n);
- }
-
- return s.toString();
- } finally {
- if (reader != null) {
- reader.close();
- }
- }
- }
-}
\ No newline at end of file
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/StringUtils.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.text.TextUtils;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
-public class StringUtils {
- private static final String LOGTAG = "GeckoStringUtils";
-
- private static final String FILTER_URL_PREFIX = "filter://";
- private static final String USER_ENTERED_URL_PREFIX = "user-entered:";
-
- /*
- * This method tries to guess if the given string could be a search query or URL,
- * and returns a previous result if there is ambiguity
- *
- * Search examples:
- * foo
- * foo bar.com
- * foo http://bar.com
- *
- * URL examples
- * foo.com
- * foo.c
- * :foo
- * http://foo.com bar
- *
- * wasSearchQuery specifies whether text was a search query before the latest change
- * in text. In ambiguous cases where the new text can be either a search or a URL,
- * wasSearchQuery is returned
- */
- public static boolean isSearchQuery(String text, boolean wasSearchQuery) {
- // We remove leading and trailing white spaces when decoding URLs
- text = text.trim();
- if (text.length() == 0)
- return wasSearchQuery;
-
- int colon = text.indexOf(':');
- int dot = text.indexOf('.');
- int space = text.indexOf(' ');
-
- // If a space is found before any dot and colon, we assume this is a search query
- if (space > -1 && (colon == -1 || space < colon) && (dot == -1 || space < dot)) {
- return true;
- }
- // Otherwise, if a dot or a colon is found, we assume this is a URL
- if (dot > -1 || colon > -1) {
- return false;
- }
- // Otherwise, text is ambiguous, and we keep its status unchanged
- return wasSearchQuery;
- }
-
- /**
- * Strip the ref from a URL, if present
- *
- * @return The base URL, without the ref. The original String is returned if it has no ref,
- * of if the input is malformed.
- */
- public static String stripRef(final String inputURL) {
- if (inputURL == null) {
- return null;
- }
-
- final int refIndex = inputURL.indexOf('#');
-
- if (refIndex >= 0) {
- return inputURL.substring(0, refIndex);
- }
-
- return inputURL;
- }
-
- public static class UrlFlags {
- public static final int NONE = 0;
- public static final int STRIP_HTTPS = 1;
- }
-
- public static String stripScheme(String url) {
- return stripScheme(url, UrlFlags.NONE);
- }
-
- public static String stripScheme(String url, int flags) {
- if (url == null) {
- return url;
- }
-
- int start = 0;
- int end = url.length();
-
- if (url.startsWith("http://")) {
- start = 7;
- } else if (url.startsWith("https://") && flags == UrlFlags.STRIP_HTTPS) {
- start = 8;
- }
-
- if (url.endsWith("/")) {
- end--;
- }
-
- return url.substring(start, end);
- }
-
- public static boolean isHttpOrHttps(String url) {
- if (TextUtils.isEmpty(url)) {
- return false;
- }
-
- return url.startsWith("http://") || url.startsWith("https://");
- }
-
- public static String stripCommonSubdomains(String host) {
- if (host == null) {
- return host;
- }
-
- // In contrast to desktop, we also strip mobile subdomains,
- // since its unlikely users are intentionally typing them
- int start = 0;
-
- if (host.startsWith("www.")) {
- start = 4;
- } else if (host.startsWith("mobile.")) {
- start = 7;
- } else if (host.startsWith("m.")) {
- start = 2;
- }
-
- return host.substring(start);
- }
-
- /**
- * Searches the url query string for the first value with the given key.
- */
- public static String getQueryParameter(String url, String desiredKey) {
- if (TextUtils.isEmpty(url) || TextUtils.isEmpty(desiredKey)) {
- return null;
- }
-
- final String[] urlParts = url.split("\\?");
- if (urlParts.length < 2) {
- return null;
- }
-
- final String query = urlParts[1];
- for (final String param : query.split("&")) {
- final String pair[] = param.split("=");
- final String key = Uri.decode(pair[0]);
-
- // Key is empty or does not match the key we're looking for, discard
- if (TextUtils.isEmpty(key) || !key.equals(desiredKey)) {
- continue;
- }
- // No value associated with key, discard
- if (pair.length < 2) {
- continue;
- }
- final String value = Uri.decode(pair[1]);
- if (TextUtils.isEmpty(value)) {
- return null;
- }
- return value;
- }
-
- return null;
- }
-
- public static boolean isFilterUrl(String url) {
- if (TextUtils.isEmpty(url)) {
- return false;
- }
-
- return url.startsWith(FILTER_URL_PREFIX);
- }
-
- public static String getFilterFromUrl(String url) {
- if (TextUtils.isEmpty(url)) {
- return null;
- }
-
- return url.substring(FILTER_URL_PREFIX.length());
- }
-
- public static boolean isShareableUrl(final String url) {
- final String scheme = Uri.parse(url).getScheme();
- return !("about".equals(scheme) || "chrome".equals(scheme) ||
- "file".equals(scheme) || "resource".equals(scheme));
- }
-
- public static boolean isUserEnteredUrl(String url) {
- return (url != null && url.startsWith(USER_ENTERED_URL_PREFIX));
- }
-
- /**
- * Given a url with a user-entered scheme, extract the
- * scheme-specific component. For e.g, given "user-entered://www.google.com",
- * this method returns "//www.google.com". If the passed url
- * does not have a user-entered scheme, the same url will be returned.
- *
- * @param url to be decoded
- * @return url component entered by user
- */
- public static String decodeUserEnteredUrl(String url) {
- Uri uri = Uri.parse(url);
- if ("user-entered".equals(uri.getScheme())) {
- return uri.getSchemeSpecificPart();
- }
- return url;
- }
-
- public static String encodeUserEnteredUrl(String url) {
- return Uri.fromParts("user-entered", url, null).toString();
- }
-
- /**
- * Compatibility layer for API < 11.
- *
- * Returns a set of the unique names of all query parameters. Iterating
- * over the set will return the names in order of their first occurrence.
- *
- * @param uri
- * @throws UnsupportedOperationException if this isn't a hierarchical URI
- *
- * @return a set of decoded names
- */
- public static Set<String> getQueryParameterNames(Uri uri) {
- if (Versions.feature11Plus) {
- return uri.getQueryParameterNames();
- }
-
- // Logic below copied from Uri.java included with Android 5.0.0.
- if (uri.isOpaque()) {
- throw new UnsupportedOperationException("This isn't a hierarchical URI.");
- }
-
- String query = uri.getEncodedQuery();
- if (query == null) {
- return Collections.emptySet();
- }
-
- Set<String> names = new LinkedHashSet<String>();
- int start = 0;
- do {
- int next = query.indexOf('&', start);
- int end = (next == -1) ? query.length() : next;
-
- int separator = query.indexOf('=', start);
- if (separator > end || separator == -1) {
- separator = end;
- }
-
- String name = query.substring(start, separator);
- names.add(Uri.decode(name));
-
- // Move start to end of name.
- start = end + 1;
- } while (start < query.length());
-
- return Collections.unmodifiableSet(names);
- }
-
- public static String safeSubstring(@NonNull final String str, final int start, final int end) {
- return str.substring(
- Math.max(0, start),
- Math.min(end, str.length()));
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/ThreadUtils.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import java.util.Map;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.MessageQueue;
-import android.util.Log;
-
-public final class ThreadUtils {
- private static final String LOGTAG = "ThreadUtils";
-
- /**
- * Controls the action taken when a method like
- * {@link ThreadUtils#assertOnUiThread(AssertBehavior)} detects a problem.
- */
- public static enum AssertBehavior {
- NONE,
- THROW,
- }
-
- private static final Thread sUiThread = Looper.getMainLooper().getThread();
- private static final Handler sUiHandler = new Handler(Looper.getMainLooper());
-
- private static volatile Thread sBackgroundThread;
-
- // Referenced directly from GeckoAppShell in highly performance-sensitive code (The extra
- // function call of the getter was harming performance. (Bug 897123))
- // Once Bug 709230 is resolved we should reconsider this as ProGuard should be able to optimise
- // this out at compile time.
- public static Handler sGeckoHandler;
- public static volatile Thread sGeckoThread;
-
- // Delayed Runnable that resets the Gecko thread priority.
- private static final Runnable sPriorityResetRunnable = new Runnable() {
- @Override
- public void run() {
- resetGeckoPriority();
- }
- };
-
- private static boolean sIsGeckoPriorityReduced;
-
- @SuppressWarnings("serial")
- public static class UiThreadBlockedException extends RuntimeException {
- public UiThreadBlockedException() {
- super();
- }
-
- public UiThreadBlockedException(String msg) {
- super(msg);
- }
-
- public UiThreadBlockedException(String msg, Throwable e) {
- super(msg, e);
- }
-
- public UiThreadBlockedException(Throwable e) {
- super(e);
- }
- }
-
- public static void dumpAllStackTraces() {
- Log.w(LOGTAG, "Dumping ALL the threads!");
- Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces();
- for (Thread t : allStacks.keySet()) {
- Log.w(LOGTAG, t.toString());
- for (StackTraceElement ste : allStacks.get(t)) {
- Log.w(LOGTAG, ste.toString());
- }
- Log.w(LOGTAG, "----");
- }
- }
-
- public static void setBackgroundThread(Thread thread) {
- sBackgroundThread = thread;
- }
-
- public static Thread getUiThread() {
- return sUiThread;
- }
-
- public static Handler getUiHandler() {
- return sUiHandler;
- }
-
- public static void postToUiThread(Runnable runnable) {
- sUiHandler.post(runnable);
- }
-
- public static void postDelayedToUiThread(Runnable runnable, long timeout) {
- sUiHandler.postDelayed(runnable, timeout);
- }
-
- public static void removeCallbacksFromUiThread(Runnable runnable) {
- sUiHandler.removeCallbacks(runnable);
- }
-
- public static Thread getBackgroundThread() {
- return sBackgroundThread;
- }
-
- public static Handler getBackgroundHandler() {
- return GeckoBackgroundThread.getHandler();
- }
-
- public static void postToBackgroundThread(Runnable runnable) {
- GeckoBackgroundThread.post(runnable);
- }
-
- public static void assertOnUiThread(final AssertBehavior assertBehavior) {
- assertOnThread(getUiThread(), assertBehavior);
- }
-
- public static void assertOnUiThread() {
- assertOnThread(getUiThread(), AssertBehavior.THROW);
- }
-
- public static void assertNotOnUiThread() {
- assertNotOnThread(getUiThread(), AssertBehavior.THROW);
- }
-
- @RobocopTarget
- public static void assertOnGeckoThread() {
- assertOnThread(sGeckoThread, AssertBehavior.THROW);
- }
-
- public static void assertNotOnGeckoThread() {
- if (sGeckoThread == null) {
- // Cannot be on Gecko thread if Gecko thread is not live yet.
- return;
- }
- assertNotOnThread(sGeckoThread, AssertBehavior.THROW);
- }
-
- public static void assertOnBackgroundThread() {
- assertOnThread(getBackgroundThread(), AssertBehavior.THROW);
- }
-
- public static void assertOnThread(final Thread expectedThread) {
- assertOnThread(expectedThread, AssertBehavior.THROW);
- }
-
- public static void assertOnThread(final Thread expectedThread, AssertBehavior behavior) {
- assertOnThreadComparison(expectedThread, behavior, true);
- }
-
- public static void assertNotOnThread(final Thread expectedThread, AssertBehavior behavior) {
- assertOnThreadComparison(expectedThread, behavior, false);
- }
-
- private static void assertOnThreadComparison(final Thread expectedThread, AssertBehavior behavior, boolean expected) {
- final Thread currentThread = Thread.currentThread();
- final long currentThreadId = currentThread.getId();
- final long expectedThreadId = expectedThread.getId();
-
- if ((currentThreadId == expectedThreadId) == expected) {
- return;
- }
-
- final String message;
- if (expected) {
- message = "Expected thread " + expectedThreadId +
- " (\"" + expectedThread.getName() + "\"), but running on thread " +
- currentThreadId + " (\"" + currentThread.getName() + "\")";
- } else {
- message = "Expected anything but " + expectedThreadId +
- " (\"" + expectedThread.getName() + "\"), but running there.";
- }
-
- final IllegalThreadStateException e = new IllegalThreadStateException(message);
-
- switch (behavior) {
- case THROW:
- throw e;
- default:
- Log.e(LOGTAG, "Method called on wrong thread!", e);
- }
- }
-
- public static boolean isOnGeckoThread() {
- if (sGeckoThread != null) {
- return isOnThread(sGeckoThread);
- }
- return false;
- }
-
- public static boolean isOnUiThread() {
- return isOnThread(getUiThread());
- }
-
- @RobocopTarget
- public static boolean isOnBackgroundThread() {
- if (sBackgroundThread == null) {
- return false;
- }
-
- return isOnThread(sBackgroundThread);
- }
-
- @RobocopTarget
- public static boolean isOnThread(Thread thread) {
- return (Thread.currentThread().getId() == thread.getId());
- }
-
- /**
- * Reduces the priority of the Gecko thread, allowing other operations
- * (such as those related to the UI and database) to take precedence.
- *
- * Note that there are no guards in place to prevent multiple calls
- * to this method from conflicting with each other.
- *
- * @param timeout Timeout in ms after which the priority will be reset
- */
- public static void reduceGeckoPriority(long timeout) {
- if (Runtime.getRuntime().availableProcessors() > 1) {
- // Don't reduce priority for multicore devices. We use availableProcessors()
- // for its fast performance. It may give false negatives (i.e. multicore
- // detected as single-core), but we can tolerate this behavior.
- return;
- }
- if (!sIsGeckoPriorityReduced && sGeckoThread != null) {
- sIsGeckoPriorityReduced = true;
- sGeckoThread.setPriority(Thread.MIN_PRIORITY);
- getUiHandler().postDelayed(sPriorityResetRunnable, timeout);
- }
- }
-
- /**
- * Resets the priority of a thread whose priority has been reduced
- * by reduceGeckoPriority.
- */
- public static void resetGeckoPriority() {
- if (sIsGeckoPriorityReduced) {
- sIsGeckoPriorityReduced = false;
- sGeckoThread.setPriority(Thread.NORM_PRIORITY);
- getUiHandler().removeCallbacks(sPriorityResetRunnable);
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/UIAsyncTask.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/* -*- 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.util;
-
-import android.os.Handler;
-import android.os.Looper;
-
-/**
- * Executes a background task and publishes the result on the UI thread.
- *
- * The standard {@link android.os.AsyncTask} only runs onPostExecute on the
- * thread it is constructed on, so this is a convenience class for creating
- * tasks off the UI thread.
- *
- * We use generics differently to Android's AsyncTask.
- * Android uses a "Params" type parameter to represent the type of all the parameters to this task.
- * It then uses arguments of type Params... to permit arbitrarily-many of these to be passed
- * fluently.
- *
- * Unfortunately, since Java does not support generic array types (and since varargs desugars to a
- * single array parameter) that behaviour exposes a hole in the type system. See:
- * http://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html#vulnerabilities
- *
- * Instead, we equivalently have a single type parameter "Param". A UiAsyncTask may take exactly one
- * parameter of type Param. Since Param can be an array type, this no more restrictive than the
- * other approach, it just provides additional type safety.
- */
-public abstract class UIAsyncTask<Param, Result> {
- /**
- * Provide a convenient API for parameter-free UiAsyncTasks by wrapping parameter-taking methods
- * from UiAsyncTask in parameterless equivalents.
- */
- public static abstract class WithoutParams<InnerResult> extends UIAsyncTask<Void, InnerResult> {
- public WithoutParams(Handler backgroundThreadHandler) {
- super(backgroundThreadHandler);
- }
-
- public void execute() {
- execute(null);
- }
-
- @Override
- protected InnerResult doInBackground(Void unused) {
- return doInBackground();
- }
-
- protected abstract InnerResult doInBackground();
- }
-
- final Handler mBackgroundThreadHandler;
- private volatile boolean mCancelled;
- private static Handler sHandler;
-
- /**
- * Creates a new asynchronous task.
- *
- * @param backgroundThreadHandler the handler to execute the background task on
- */
- public UIAsyncTask(Handler backgroundThreadHandler) {
- mBackgroundThreadHandler = backgroundThreadHandler;
- }
-
- private static synchronized Handler getUiHandler() {
- if (sHandler == null) {
- sHandler = new Handler(Looper.getMainLooper());
- }
-
- return sHandler;
- }
-
- private final class BackgroundTaskRunnable implements Runnable {
- private final Param mParam;
-
- public BackgroundTaskRunnable(Param param) {
- mParam = param;
- }
-
- @Override
- public void run() {
- final Result result = doInBackground(mParam);
-
- getUiHandler().post(new Runnable() {
- @Override
- public void run() {
- if (mCancelled) {
- onCancelled();
- } else {
- onPostExecute(result);
- }
- }
- });
- }
- }
-
- protected void execute(final Param param) {
- getUiHandler().post(new Runnable() {
- @Override
- public void run() {
- onPreExecute();
- mBackgroundThreadHandler.post(new BackgroundTaskRunnable(param));
- }
- });
- }
-
- public final boolean cancel() {
- mCancelled = true;
- return mCancelled;
- }
-
- public final boolean isCancelled() {
- return mCancelled;
- }
-
- protected void onPreExecute() { }
- protected void onPostExecute(Result result) { }
- protected void onCancelled() { }
- protected abstract Result doInBackground(Param param);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/UUIDUtil.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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.util;
-
-import java.util.regex.Pattern;
-
-/**
- * Utilities for UUIDs.
- */
-public class UUIDUtil {
- private UUIDUtil() {}
-
- public static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
- public static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/WeakReferenceHandler.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* 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.util;
-
-import android.os.Handler;
-
-import java.lang.ref.WeakReference;
-
-/**
- * A Handler to help prevent memory leaks when using Handlers as inner classes.
- *
- * To use, extend the Handler, if it's an inner class, make it static,
- * and reference `this` via the associated WeakReference.
- *
- * For additional context, see the "HandlerLeak" android lint item and this post by Romain Guy:
- * https://groups.google.com/forum/#!msg/android-developers/1aPZXZG6kWk/lIYDavGYn5UJ
- */
-public class WeakReferenceHandler<T> extends Handler {
- public final WeakReference<T> mTarget;
-
- public WeakReferenceHandler(final T that) {
- super();
- mTarget = new WeakReference<>(that);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/WebActivityMapper.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/* -*- 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.util;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-public final class WebActivityMapper {
- private static final String LOGTAG = "Gecko";
-
- private static final Map<String, WebActivityMapping> activityMap = new HashMap<String, WebActivityMapping>();
- static {
- activityMap.put("dial", new DialMapping());
- activityMap.put("open", new OpenMapping());
- activityMap.put("pick", new PickMapping());
- activityMap.put("send", new SendMapping());
- activityMap.put("view", new ViewMapping());
- activityMap.put("record", new RecordMapping());
- };
-
- private static abstract class WebActivityMapping {
- protected JSONObject mData;
-
- public void setData(JSONObject data) {
- mData = data;
- }
-
- // Cannot return null
- public abstract String getAction();
-
- public String getMime() throws JSONException {
- return null;
- }
-
- public String getUri() throws JSONException {
- return null;
- }
-
- public void putExtras(Intent intent) throws JSONException {}
- }
-
- /**
- * Provides useful defaults for mime type and uri.
- */
- private static abstract class BaseMapping extends WebActivityMapping {
- /**
- * If 'type' is present in data object, uses the value as the MIME type.
- */
- @Override
- public String getMime() throws JSONException {
- return mData.optString("type", null);
- }
-
- /**
- * If 'uri' or 'url' is present in data object, uses the respective value as the Uri.
- */
- @Override
- public String getUri() throws JSONException {
- // Will return uri or url if present.
- String uri = mData.optString("uri", null);
- return uri != null ? uri : mData.optString("url", null);
- }
- }
-
- public static Intent getIntentForWebActivity(JSONObject message) throws JSONException {
- final String name = message.getString("name").toLowerCase();
- final JSONObject data = message.getJSONObject("data");
-
- Log.w(LOGTAG, "Activity is: " + name);
- final WebActivityMapping mapping = activityMap.get(name);
- if (mapping == null) {
- Log.w(LOGTAG, "No mapping found!");
- return null;
- }
-
- mapping.setData(data);
-
- final Intent intent = new Intent(mapping.getAction());
-
- final String mime = mapping.getMime();
- if (!TextUtils.isEmpty(mime)) {
- intent.setType(mime);
- }
-
- final String uri = mapping.getUri();
- if (!TextUtils.isEmpty(uri)) {
- intent.setData(Uri.parse(uri));
- }
-
- mapping.putExtras(intent);
-
- return intent;
- }
-
- private static class DialMapping extends WebActivityMapping {
- @Override
- public String getAction() {
- return Intent.ACTION_DIAL;
- }
-
- @Override
- public String getUri() throws JSONException {
- return "tel:" + mData.getString("number");
- }
- }
-
- private static class OpenMapping extends BaseMapping {
- @Override
- public String getAction() {
- return Intent.ACTION_VIEW;
- }
- }
-
- private static class PickMapping extends BaseMapping {
- @Override
- public String getAction() {
- return Intent.ACTION_GET_CONTENT;
- }
-
- @Override
- public String getMime() throws JSONException {
- // bug 1007112 - pick action needs a mimetype to work
- String mime = mData.optString("type", null);
- return !TextUtils.isEmpty(mime) ? mime : "*/*";
- }
- }
-
- private static class SendMapping extends BaseMapping {
- @Override
- public String getAction() {
- return Intent.ACTION_SEND;
- }
-
- @Override
- public void putExtras(Intent intent) throws JSONException {
- optPutExtra("text", Intent.EXTRA_TEXT, intent);
- optPutExtra("html_text", Intent.EXTRA_HTML_TEXT, intent);
- optPutExtra("stream", Intent.EXTRA_STREAM, intent);
- }
-
- private void optPutExtra(String key, String extraName, Intent intent) {
- final String extraValue = mData.optString(key);
- if (!TextUtils.isEmpty(extraValue)) {
- intent.putExtra(extraName, extraValue);
- }
- }
- }
-
- private static class ViewMapping extends BaseMapping {
- @Override
- public String getAction() {
- return Intent.ACTION_VIEW;
- }
-
- @Override
- public String getMime() {
- // MozActivity adds a type 'url' here, we don't want to set the MIME to 'url'.
- String type = mData.optString("type", null);
- if ("url".equals(type) || "uri".equals(type)) {
- return null;
- } else {
- return type;
- }
- }
- }
-
- private static class RecordMapping extends WebActivityMapping {
- @Override
- public String getAction() {
- String type = mData.optString("type", null);
- if ("photos".equals(type)) {
- return "android.media.action.IMAGE_CAPTURE";
- } else if ("videos".equals(type)) {
- return "android.media.action.VIDEO_CAPTURE";
- }
- return null;
- }
-
- // Add an extra to specify where to save the picture/video.
- @Override
- public void putExtras(Intent intent) {
- final String action = getAction();
-
- final String dirType = action == "android.media.action.IMAGE_CAPTURE"
- ? Environment.DIRECTORY_PICTURES
- : Environment.DIRECTORY_MOVIES;
-
- final String ext = action == "android.media.action.IMAGE_CAPTURE"
- ? ".jpg"
- : ".mp4";
-
- File destDir = Environment.getExternalStoragePublicDirectory(dirType);
-
- try {
- File dest = File.createTempFile(
- "capture", /* prefix */
- ext, /* suffix */
- destDir /* directory */
- );
- intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(dest));
- } catch (Exception e) {
- Log.w(LOGTAG, "Failed to add extra for " + action + " : " + e);
- }
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/util/WindowUtils.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.content.Context;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.Display;
-import android.view.WindowManager;
-
-import java.lang.reflect.Method;
-
-public class WindowUtils {
- private static final String LOGTAG = "Gecko" + WindowUtils.class.getSimpleName();
-
- private WindowUtils() { /* To prevent instantiation */ }
-
- /**
- * Returns the best-guess physical device dimensions, including the system status bars. Note
- * that DisplayMetrics.height/widthPixels does not include the system bars.
- *
- * via http://stackoverflow.com/a/23861333
- *
- * @param context the calling Activity's Context
- * @return The number of pixels of the device's largest dimension, ignoring software status bars
- */
- public static int getLargestDimension(final Context context) {
- final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
-
- if (Versions.feature17Plus) {
- final DisplayMetrics realMetrics = new DisplayMetrics();
- display.getRealMetrics(realMetrics);
- return Math.max(realMetrics.widthPixels, realMetrics.heightPixels);
-
- } else if (Versions.feature14Plus) {
- int tempWidth;
- int tempHeight;
- try {
- final Method getRawH = Display.class.getMethod("getRawHeight");
- final Method getRawW = Display.class.getMethod("getRawWidth");
- tempWidth = (Integer) getRawW.invoke(display);
- tempHeight = (Integer) getRawH.invoke(display);
- } catch (Exception e) {
- // This is the best we can do.
- tempWidth = display.getWidth();
- tempHeight = display.getHeight();
- Log.w(LOGTAG, "Couldn't use reflection to get the real display metrics.");
- }
-
- return Math.max(tempWidth, tempHeight);
-
- } else {
- // This should be close, as lower API devices should not have window navigation bars.
- return Math.max(display.getWidth(), display.getHeight());
- }
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
- * Copyright 2012 Roman Nurik
- *
- * 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.widget;
-
-import android.graphics.Rect;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.AbsListView.RecyclerListener;
-import android.widget.ListView;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.view.ViewPropertyAnimator;
-
-import org.mozilla.gecko.R;
-
-/**
- * This code is based off of Jake Wharton's NOA port (https://github.com/JakeWharton/SwipeToDismissNOA)
- * of Roman Nurik's SwipeToDismiss library. It has been modified for better support with async
- * adapters.
- *
- * A {@link android.view.View.OnTouchListener} that makes the list items in a {@link ListView}
- * dismissable. {@link ListView} is given special treatment because by default it handles touches
- * for its list items... i.e. it's in charge of drawing the pressed state (the list selector),
- * handling list item clicks, etc.
- *
- * <p>After creating the listener, the caller should also call
- * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing
- * in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is
- * already assigned, the caller should still pass scroll changes through to this listener. This will
- * ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view
- * scrolling.</p>
- *
- * <p>Example usage:</p>
- *
- * <pre>
- * SwipeDismissListViewTouchListener touchListener =
- * new SwipeDismissListViewTouchListener(
- * listView,
- * new SwipeDismissListViewTouchListener.OnDismissCallback() {
- * public void onDismiss(ListView listView, int[] reverseSortedPositions) {
- * for (int position : reverseSortedPositions) {
- * adapter.remove(adapter.getItem(position));
- * }
- * adapter.notifyDataSetChanged();
- * }
- * });
- * listView.setOnTouchListener(touchListener);
- * listView.setOnScrollListener(touchListener.makeScrollListener());
- * </pre>
- *
- * <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
- * see {@link SwipeDismissTouchListener}.</p>
- *
- * @see SwipeDismissTouchListener
- */
-public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
- // Cached ViewConfiguration and system-wide constant values
- private final int mSlop;
- private final int mMinFlingVelocity;
- private final int mMaxFlingVelocity;
- private final long mAnimationTime;
-
- // Fixed properties
- private final ListView mListView;
- private final OnDismissCallback mCallback;
- private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
-
- // Transient properties
- private float mDownX;
- private boolean mSwiping;
- private VelocityTracker mVelocityTracker;
- private int mDownPosition;
- private View mDownView;
- private boolean mPaused;
- private boolean mDismissing;
-
- /**
- * The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
- * about a successful dismissal of a list item.
- */
- public interface OnDismissCallback {
- /**
- * Called when the user has indicated they she would like to dismiss one or more list item
- * positions.
- *
- * @param listView The originating {@link ListView}.
- * @param position The position being dismissed.
- */
- void onDismiss(ListView listView, int position);
- }
-
- /**
- * Constructs a new swipe-to-dismiss touch listener for the given list view.
- *
- * @param listView The list view whose items should be dismissable.
- * @param callback The callback to trigger when the user has indicated that she would like to
- * dismiss one or more list items.
- */
- public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
- ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
- mSlop = vc.getScaledTouchSlop();
- mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
- mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
- mAnimationTime = listView.getContext().getResources().getInteger(
- android.R.integer.config_shortAnimTime);
- mListView = listView;
- mCallback = callback;
- }
-
- /**
- * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
- *
- * @param enabled Whether or not to watch for gestures.
- */
- public void setEnabled(boolean enabled) {
- mPaused = !enabled;
- }
-
- /**
- * Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the
- * {@link ListView} using
- * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
- * If a scroll listener is already assigned, the caller should still pass scroll changes
- * through to this listener. This will ensure that this
- * {@link SwipeDismissListViewTouchListener} is paused during list view scrolling.</p>
- *
- * @see {@link SwipeDismissListViewTouchListener}
- */
- public AbsListView.OnScrollListener makeScrollListener() {
- return new AbsListView.OnScrollListener() {
- @Override
- public void onScrollStateChanged(AbsListView absListView, int scrollState) {
- setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
- }
-
- @Override
- public void onScroll(AbsListView absListView, int i, int i1, int i2) {
- }
- };
- }
-
- /**
- * Returns a {@link android.widget.AbsListView.RecyclerListener} to be added to the
- * {@link ListView} using {@link ListView#setRecyclerListener(RecyclerListener)}.
- */
- public AbsListView.RecyclerListener makeRecyclerListener() {
- return new AbsListView.RecyclerListener() {
- @Override
- public void onMovedToScrapHeap(View view) {
- final Object tag = view.getTag(R.id.original_height);
-
- // To reset the view to the correct height after its animation, the view's height
- // is stored in its tag. Reset the view here.
- if (tag instanceof Integer) {
- view.setAlpha(1f);
- view.setTranslationX(0);
- final ViewGroup.LayoutParams lp = view.getLayoutParams();
- lp.height = (int) tag;
- view.setLayoutParams(lp);
- view.setTag(R.id.original_height, null);
- }
- }
- };
- }
-
- @Override
- public boolean onTouch(View view, MotionEvent motionEvent) {
- if (mViewWidth < 2) {
- mViewWidth = mListView.getWidth();
- }
-
- switch (motionEvent.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- if (mPaused) {
- return false;
- }
-
- if (mDismissing) {
- return true;
- }
-
- // TODO: ensure this is a finger, and set a flag
-
- // Find the child view that was touched (perform a hit test)
- Rect rect = new Rect();
- int childCount = mListView.getChildCount();
- int[] listViewCoords = new int[2];
- mListView.getLocationOnScreen(listViewCoords);
- int x = (int) motionEvent.getRawX() - listViewCoords[0];
- int y = (int) motionEvent.getRawY() - listViewCoords[1];
- View child;
- for (int i = 0; i < childCount; i++) {
- child = mListView.getChildAt(i);
- child.getHitRect(rect);
- if (rect.contains(x, y)) {
- mDownView = child;
- break;
- }
- }
-
- if (mDownView != null) {
- mDownX = motionEvent.getRawX();
- mDownPosition = mListView.getPositionForView(mDownView);
-
- mVelocityTracker = VelocityTracker.obtain();
- mVelocityTracker.addMovement(motionEvent);
- }
- view.onTouchEvent(motionEvent);
- return true;
- }
-
- case MotionEvent.ACTION_UP: {
- if (mVelocityTracker == null) {
- break;
- }
-
- float deltaX = motionEvent.getRawX() - mDownX;
- mVelocityTracker.addMovement(motionEvent);
- mVelocityTracker.computeCurrentVelocity(1000);
- float velocityX = Math.abs(mVelocityTracker.getXVelocity());
- float velocityY = Math.abs(mVelocityTracker.getYVelocity());
- boolean dismiss = false;
- boolean dismissRight = false;
- if (Math.abs(deltaX) > mViewWidth / 2) {
- dismiss = true;
- dismissRight = deltaX > 0;
- } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
- && velocityY < velocityX) {
- dismiss = true;
- dismissRight = mVelocityTracker.getXVelocity() > 0;
- }
- if (dismiss) {
- // dismiss
- mDismissing = true;
- final View downView = mDownView; // mDownView gets null'd before animation ends
- final int downPosition = mDownPosition;
- mDownView.animate()
- .translationX(dismissRight ? mViewWidth : -mViewWidth)
- .alpha(0)
- .setDuration(mAnimationTime)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- performDismiss(downView, downPosition);
- }
- });
- } else {
- // cancel
- mDownView.animate()
- .translationX(0)
- .alpha(1)
- .setDuration(mAnimationTime)
- .setListener(null);
- }
-
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
-
- mDownX = 0;
- mDownView = null;
- mDownPosition = ListView.INVALID_POSITION;
- mSwiping = false;
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- if (mVelocityTracker == null || mPaused) {
- break;
- }
-
- mVelocityTracker.addMovement(motionEvent);
- float deltaX = motionEvent.getRawX() - mDownX;
- if (Math.abs(deltaX) > mSlop) {
- mSwiping = true;
- mListView.requestDisallowInterceptTouchEvent(true);
-
- // Cancel ListView's touch (un-highlighting the item)
- MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
- cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
- (motionEvent.getActionIndex()
- << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
- mListView.onTouchEvent(cancelEvent);
- cancelEvent.recycle();
- }
-
- if (mSwiping) {
- mDownView.setTranslationX(deltaX);
- mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth)));
- return true;
- }
- break;
- }
- }
- return false;
- }
-
- /**
- * Animate the dismissed list item to zero-height and fire the dismiss callback when it finishes.
- *
- * @param dismissView ListView item to dismiss
- * @param dismissPosition Position of dismissed item
- */
- private void performDismiss(final View dismissView, final int dismissPosition) {
- final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
- final int originalHeight = lp.height;
-
- ValueAnimator animator = ValueAnimator.ofInt(dismissView.getHeight(), 1).setDuration(mAnimationTime);
-
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // Since the view is still a part of the ListView, we can't reset the animated
- // properties yet; otherwise, the view would briefly reappear. Store the original
- // height in the view's tag to flag it for the recycler. This is racy since the user
- // could scroll the dismissed view off the screen, then back on the screen, before
- // it's removed from the adapter, causing the dismissed view to briefly reappear.
- dismissView.setTag(R.id.original_height, originalHeight);
-
- mCallback.onDismiss(mListView, dismissPosition);
- mDismissing = false;
- }
- });
-
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- lp.height = (Integer) valueAnimator.getAnimatedValue();
- dismissView.setLayoutParams(lp);
- }
- });
-
- animator.start();
- }
-}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -5,16 +5,17 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ['locales']
CONFIGURE_SUBST_FILES += ['adjust_sdk_app_token']
include('android-services.mozbuild')
+geckoview_source_dir = TOPSRCDIR + '/mobile/android/geckoview/src/main/'
thirdparty_source_dir = TOPSRCDIR + '/mobile/android/thirdparty/'
constants_jar = add_java_jar('constants')
constants_jar.sources = ['java/org/mozilla/gecko/' + x for x in [
'adjust/AdjustHelperInterface.java',
'annotation/JNITarget.java',
'annotation/ReflectionTarget.java',
'annotation/RobocopTarget.java',
@@ -77,33 +78,33 @@ if CONFIG['ANDROID_RECYCLERVIEW_V7_AAR']
if CONFIG['ANDROID_CUSTOMTABS_AAR']:
ANDROID_EXTRA_PACKAGES += ['android.support.customtabs']
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_CUSTOMTABS_AAR_RES']]
resjar.generated_sources += ['android/support/customtabs/R.java']
resjar.javac_flags += ['-Xlint:all']
mgjar = add_java_jar('gecko-mozglue')
-mgjar.sources += ['java/org/mozilla/gecko/' + x for x in [
+mgjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [
'mozglue/ByteBufferInputStream.java',
'mozglue/DirectBufferAllocator.java',
'mozglue/GeckoLoader.java',
'mozglue/JNIObject.java',
'mozglue/NativeReference.java',
'mozglue/NativeZip.java',
'mozglue/SafeIntent.java',
]]
mgjar.generated_sources = [] # Keep it this way.
mgjar.extra_jars += [
'constants.jar',
]
mgjar.javac_flags += ['-Xlint:all']
gujar = add_java_jar('gecko-util')
-gujar.sources += ['java/org/mozilla/gecko/' + x for x in [
+gujar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [
'util/ActivityResultHandler.java',
'util/ActivityResultHandlerMap.java',
'util/ActivityUtils.java',
'util/BundleEventListener.java',
'util/Clipboard.java',
'util/ContextUtils.java',
'util/DateUtil.java',
'util/DrawableUtil.java',
@@ -192,42 +193,124 @@ if CONFIG['MOZ_WEBRTC']:
'constants.jar',
'gecko-R.jar',
'gecko-browser.jar',
'gecko-util.jar',
'gecko-mozglue.jar',
]
wrjar.javac_flags += ['-Xlint:all,-deprecation,-cast']
+geckoview_java_files = [
+ 'AlarmReceiver.java',
+ 'AndroidGamepadManager.java',
+ 'ANRReporter.java',
+ 'BaseGeckoInterface.java',
+ 'ContextGetter.java',
+ 'CrashHandler.java',
+ 'EventDispatcher.java',
+ 'GeckoAccessibility.java',
+ 'GeckoAppShell.java',
+ 'GeckoBatteryManager.java',
+ 'GeckoEditable.java',
+ 'GeckoEditableClient.java',
+ 'GeckoEditableListener.java',
+ 'GeckoEvent.java',
+ 'GeckoHalDefines.java',
+ 'GeckoInputConnection.java',
+ 'GeckoJavaSampler.java',
+ 'GeckoNetworkManager.java',
+ 'GeckoProfile.java',
+ 'GeckoProfileDirectories.java',
+ 'GeckoScreenOrientation.java',
+ 'GeckoService.java',
+ 'GeckoSharedPrefs.java',
+ 'GeckoSmsManager.java',
+ 'GeckoThread.java',
+ 'GeckoView.java',
+ 'gfx/BitmapUtils.java',
+ 'gfx/DisplayPortCalculator.java',
+ 'gfx/DisplayPortMetrics.java',
+ 'gfx/DrawTimingQueue.java',
+ 'gfx/DynamicToolbarAnimator.java',
+ 'gfx/FloatSize.java',
+ 'gfx/FullScreenState.java',
+ 'gfx/GeckoLayerClient.java',
+ 'gfx/GLController.java',
+ 'gfx/ImmutableViewportMetrics.java',
+ 'gfx/IntSize.java',
+ 'gfx/Layer.java',
+ 'gfx/LayerRenderer.java',
+ 'gfx/LayerView.java',
+ 'gfx/NativePanZoomController.java',
+ 'gfx/Overscroll.java',
+ 'gfx/OverscrollEdgeEffect.java',
+ 'gfx/PanningPerfAPI.java',
+ 'gfx/PanZoomController.java',
+ 'gfx/PanZoomTarget.java',
+ 'gfx/PluginLayer.java',
+ 'gfx/PointUtils.java',
+ 'gfx/ProgressiveUpdateData.java',
+ 'gfx/RectUtils.java',
+ 'gfx/RenderTask.java',
+ 'gfx/TextureGenerator.java',
+ 'gfx/TextureReaper.java',
+ 'gfx/ViewTransform.java',
+ 'gfx/VirtualLayer.java',
+ 'InputConnectionListener.java',
+ 'InputMethods.java',
+ 'NotificationClient.java',
+ 'NotificationHandler.java',
+ 'NotificationService.java',
+ 'NSSBridge.java',
+ 'permissions/PermissionBlock.java',
+ 'permissions/Permissions.java',
+ 'permissions/PermissionsHelper.java',
+ 'PrefsHelper.java',
+ 'restrictions/DefaultConfiguration.java',
+ 'restrictions/GuestProfileConfiguration.java',
+ 'restrictions/Restrictable.java',
+ 'restrictions/RestrictedProfileConfiguration.java',
+ 'restrictions/RestrictionCache.java',
+ 'restrictions/RestrictionConfiguration.java',
+ 'restrictions/RestrictionProvider.java',
+ 'restrictions/Restrictions.java',
+ 'ServiceNotificationClient.java',
+ 'SmsManager.java',
+ 'sqlite/ByteBufferInputStream.java',
+ 'sqlite/MatrixBlobCursor.java',
+ 'sqlite/SQLiteBridge.java',
+ 'sqlite/SQLiteBridgeException.java',
+ 'SurfaceBits.java',
+ 'TouchEventInterceptor.java',
+ 'widget/SwipeDismissListViewTouchListener.java',
+]
+
gbjar = add_java_jar('gecko-browser')
+gbjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x
+ for x in geckoview_java_files]
+
gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'AboutPages.java',
'AccountsHelper.java',
'ActionBarTextSelection.java',
'ActionModeCompat.java',
'ActionModeCompatView.java',
'ActivityHandlerHelper.java',
- 'AlarmReceiver.java',
- 'AndroidGamepadManager.java',
'animation/AnimationUtils.java',
'animation/HeightChangeAnimation.java',
'animation/PropertyAnimator.java',
'animation/Rotate3DAnimation.java',
'animation/ViewHelper.java',
- 'ANRReporter.java',
'AppNotificationClient.java',
- 'BaseGeckoInterface.java',
'BootReceiver.java',
'BrowserApp.java',
'BrowserLocaleManager.java',
'cleanup/FileCleanupController.java',
'cleanup/FileCleanupService.java',
'ContactService.java',
- 'ContextGetter.java',
- 'CrashHandler.java',
'CustomEditText.java',
'customtabs/CustomTabsActivity.java',
'customtabs/GeckoCustomTabsService.java',
'DataReportingNotification.java',
'db/AbstractPerProfileDatabaseProvider.java',
'db/AbstractTransactionalProvider.java',
'db/BaseTable.java',
'db/BrowserDatabaseHelper.java',
@@ -277,17 +360,16 @@ gbjar.sources += ['java/org/mozilla/geck
'dlc/DownloadContentService.java',
'dlc/StudyAction.java',
'dlc/SyncAction.java',
'dlc/VerifyAction.java',
'DoorHangerPopup.java',
'DownloadsIntegration.java',
'DynamicToolbar.java',
'EditBookmarkDialog.java',
- 'EventDispatcher.java',
'Experiments.java',
'favicons/cache/FaviconCache.java',
'favicons/cache/FaviconCacheElement.java',
'favicons/cache/FaviconsForURL.java',
'favicons/decoders/FaviconDecoder.java',
'favicons/decoders/ICODecoder.java',
'favicons/decoders/IconDirectoryEntry.java',
'favicons/decoders/LoadFaviconResult.java',
@@ -322,73 +404,23 @@ gbjar.sources += ['java/org/mozilla/geck
'firstrun/FirstrunAnimationContainer.java',
'firstrun/FirstrunPager.java',
'firstrun/FirstrunPagerConfig.java',
'firstrun/FirstrunPanel.java',
'firstrun/RestrictedWelcomePanel.java',
'firstrun/SyncPanel.java',
'firstrun/TabQueuePanel.java',
'FormAssistPopup.java',
- 'GeckoAccessibility.java',
'GeckoActivity.java',
'GeckoActivityStatus.java',
'GeckoApp.java',
'GeckoApplication.java',
- 'GeckoAppShell.java',
- 'GeckoBatteryManager.java',
- 'GeckoEditable.java',
- 'GeckoEditableClient.java',
- 'GeckoEditableListener.java',
- 'GeckoEvent.java',
- 'GeckoHalDefines.java',
- 'GeckoInputConnection.java',
- 'GeckoJavaSampler.java',
'GeckoMessageReceiver.java',
- 'GeckoNetworkManager.java',
- 'GeckoProfile.java',
- 'GeckoProfileDirectories.java',
'GeckoProfilesProvider.java',
- 'GeckoScreenOrientation.java',
- 'GeckoService.java',
- 'GeckoSharedPrefs.java',
- 'GeckoSmsManager.java',
- 'GeckoThread.java',
'GeckoUpdateReceiver.java',
- 'GeckoView.java',
- 'GeckoViewChrome.java',
- 'GeckoViewContent.java',
- 'gfx/BitmapUtils.java',
- 'gfx/DisplayPortCalculator.java',
- 'gfx/DisplayPortMetrics.java',
- 'gfx/DrawTimingQueue.java',
- 'gfx/DynamicToolbarAnimator.java',
- 'gfx/FloatSize.java',
- 'gfx/FullScreenState.java',
- 'gfx/GeckoLayerClient.java',
- 'gfx/GLController.java',
- 'gfx/ImmutableViewportMetrics.java',
- 'gfx/IntSize.java',
- 'gfx/Layer.java',
- 'gfx/LayerRenderer.java',
- 'gfx/LayerView.java',
- 'gfx/NativePanZoomController.java',
- 'gfx/Overscroll.java',
- 'gfx/OverscrollEdgeEffect.java',
- 'gfx/PanningPerfAPI.java',
- 'gfx/PanZoomController.java',
- 'gfx/PanZoomTarget.java',
- 'gfx/PluginLayer.java',
- 'gfx/PointUtils.java',
- 'gfx/ProgressiveUpdateData.java',
- 'gfx/RectUtils.java',
- 'gfx/RenderTask.java',
- 'gfx/TextureGenerator.java',
- 'gfx/TextureReaper.java',
- 'gfx/ViewTransform.java',
- 'gfx/VirtualLayer.java',
'GlobalHistory.java',
'GuestSession.java',
'health/HealthRecorder.java',
'health/SessionInformation.java',
'health/StubbedHealthRecorder.java',
'home/BookmarkFolderView.java',
'home/BookmarkScreenshotRow.java',
'home/BookmarksListAdapter.java',
@@ -441,18 +473,16 @@ gbjar.sources += ['java/org/mozilla/geck
'home/SpacingDecoration.java',
'home/TabMenuStrip.java',
'home/TabMenuStripLayout.java',
'home/TopSitesGridItemView.java',
'home/TopSitesGridView.java',
'home/TopSitesPanel.java',
'home/TopSitesThumbnailView.java',
'home/TwoLinePageRow.java',
- 'InputConnectionListener.java',
- 'InputMethods.java',
'IntentHelper.java',
'javaaddons/JavaAddonManager.java',
'javaaddons/JavaAddonManagerV1.java',
'LauncherActivity.java',
'lwt/LightweightTheme.java',
'lwt/LightweightThemeDrawable.java',
'mdns/MulticastDNSManager.java',
'media/AudioFocusAgent.java',
@@ -464,37 +494,30 @@ gbjar.sources += ['java/org/mozilla/geck
'menu/GeckoMenuItem.java',
'menu/GeckoSubMenu.java',
'menu/MenuItemActionBar.java',
'menu/MenuItemDefault.java',
'menu/MenuItemSwitcherLayout.java',
'menu/MenuPanel.java',
'menu/MenuPopup.java',
'MotionEventInterceptor.java',
- 'NotificationClient.java',
- 'NotificationHandler.java',
'NotificationHelper.java',
'notifications/WhatsNewReceiver.java',
- 'NotificationService.java',
- 'NSSBridge.java',
'OrderedBroadcastHelper.java',
'overlays/OverlayConstants.java',
'overlays/service/OverlayActionService.java',
'overlays/service/ShareData.java',
'overlays/service/sharemethods/AddBookmark.java',
'overlays/service/sharemethods/SendTab.java',
'overlays/service/sharemethods/ShareMethod.java',
'overlays/ui/OverlayDialogButton.java',
'overlays/ui/SendTabDeviceListArrayAdapter.java',
'overlays/ui/SendTabList.java',
'overlays/ui/SendTabTargetSelectedListener.java',
'overlays/ui/ShareDialog.java',
- 'permissions/PermissionBlock.java',
- 'permissions/Permissions.java',
- 'permissions/PermissionsHelper.java',
'preferences/AlignRightLinkPreference.java',
'preferences/AndroidImport.java',
'preferences/AndroidImportPreference.java',
'preferences/AppCompatPreferenceActivity.java',
'preferences/ClearOnShutdownPref.java',
'preferences/CustomCheckBoxPreference.java',
'preferences/CustomListCategory.java',
'preferences/CustomListPreference.java',
@@ -510,17 +533,16 @@ gbjar.sources += ['java/org/mozilla/geck
'preferences/MultiPrefMultiChoicePreference.java',
'preferences/PanelsPreference.java',
'preferences/PanelsPreferenceCategory.java',
'preferences/PrivateDataPreference.java',
'preferences/SearchEnginePreference.java',
'preferences/SearchPreferenceCategory.java',
'preferences/SetHomepagePreference.java',
'preferences/SyncPreference.java',
- 'PrefsHelper.java',
'PrintHelper.java',
'PrivateTab.java',
'promotion/AddToHomeScreenPromotion.java',
'promotion/HomeScreenPrompt.java',
'promotion/ReaderViewBookmarkPromotion.java',
'promotion/SimpleHelperUI.java',
'prompts/ColorPickerInput.java',
'prompts/IconGridInput.java',
@@ -532,39 +554,24 @@ gbjar.sources += ['java/org/mozilla/geck
'prompts/PromptListItem.java',
'prompts/PromptService.java',
'prompts/TabInput.java',
'reader/ReaderModeUtils.java',
'reader/ReadingListHelper.java',
'reader/SavedReaderViewHelper.java',
'RemoteClientsDialogFragment.java',
'Restarter.java',
- 'restrictions/DefaultConfiguration.java',
- 'restrictions/GuestProfileConfiguration.java',
- 'restrictions/Restrictable.java',
- 'restrictions/RestrictedProfileConfiguration.java',
- 'restrictions/RestrictionCache.java',
- 'restrictions/RestrictionConfiguration.java',
- 'restrictions/RestrictionProvider.java',
- 'restrictions/Restrictions.java',
'ScreenshotObserver.java',
'search/SearchEngine.java',
'search/SearchEngineManager.java',
- 'ServiceNotificationClient.java',
'SessionParser.java',
'SharedPreferencesHelper.java',
'SiteIdentity.java',
- 'SmsManager.java',
'SnackbarHelper.java',
- 'sqlite/ByteBufferInputStream.java',
- 'sqlite/MatrixBlobCursor.java',
- 'sqlite/SQLiteBridge.java',
- 'sqlite/SQLiteBridgeException.java',
'SuggestClient.java',
- 'SurfaceBits.java',
'Tab.java',
'tabqueue/TabQueueHelper.java',
'tabqueue/TabQueuePrompt.java',
'tabqueue/TabQueueService.java',
'tabqueue/TabReceivedService.java',
'Tabs.java',
'tabs/PrivateTabsPanel.java',
'tabs/TabCurve.java',
@@ -617,17 +624,16 @@ gbjar.sources += ['java/org/mozilla/geck
'toolbar/ShapedButtonFrameLayout.java',
'toolbar/SiteIdentityPopup.java',
'toolbar/TabCounter.java',
'toolbar/ToolbarDisplayLayout.java',
'toolbar/ToolbarEditLayout.java',
'toolbar/ToolbarEditText.java',
'toolbar/ToolbarPrefs.java',
'toolbar/ToolbarProgressView.java',
- 'TouchEventInterceptor.java',
'trackingprotection/TrackingProtectionPrompt.java',
'updater/PostUpdateHandler.java',
'updater/UpdateService.java',
'updater/UpdateServiceHelper.java',
'widget/ActivityChooserModel.java',
'widget/AllCapsTextView.java',
'widget/AnchoredPopup.java',
'widget/AnimatedHeightLayout.java',
@@ -653,17 +659,16 @@ gbjar.sources += ['java/org/mozilla/geck
'widget/IconTabWidget.java',
'widget/LoginDoorHanger.java',
'widget/RecyclerViewClickSupport.java',
'widget/ResizablePathDrawable.java',
'widget/RoundedCornerLayout.java',
'widget/SiteLogins.java',
'widget/SquaredImageView.java',
'widget/SquaredRelativeLayout.java',
- 'widget/SwipeDismissListViewTouchListener.java',
'widget/TabThumbnailWrapper.java',
'widget/ThumbnailView.java',
'widget/TwoWayView.java',
'ZoomedView.java',
]]
# The following sources are checked in to version control but
# generated by a script (java/org/mozilla/gecko/widget/themed/generate_themed_views.py).
# If you're editing this list, make sure to edit that script.
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/build.gradle
@@ -0,0 +1,138 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
+
+apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ targetSdkVersion 23
+ minSdkVersion 15
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ dexOptions {
+ javaMaxHeapSize "2g"
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ sourceSets {
+ main {
+ java {
+ srcDir "${topsrcdir}/mobile/android/geckoview/src/duplicate/java"
+
+ // TODO: support WebRTC.
+ // if (mozconfig.substs.MOZ_WEBRTC) {
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
+ // }
+
+ // TODO: don't use AppConstants.
+ srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
+ exclude 'org/mozilla/gecko/AdjustConstants.java'
+ }
+
+ assets {
+ }
+ }
+ }
+}
+
+dependencies {
+ compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+}
+
+task syncOmnijarFromDistDir(type: Sync) {
+ into("${project.buildDir}/generated/omnijar")
+ from("${topobjdir}/dist/fennec/assets") {
+ include 'omni.ja'
+ }
+}
+
+task checkLibsExistInDistDir<< {
+ if (syncLibsFromDistDir.source.empty) {
+ throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib. Have you built and packaged?")
+ }
+}
+
+task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
+ into("${project.buildDir}/generated/jniLibs")
+ from("${topobjdir}/dist/fennec/lib")
+}
+
+task checkAssetsExistInDistDir<< {
+ if (syncAssetsFromDistDir.source.empty) {
+ throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets. Have you built and packaged?")
+ }
+}
+
+task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
+ into("${project.buildDir}/generated/assets")
+ from("${topobjdir}/dist/fennec/assets") {
+ exclude 'omni.ja'
+ }
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+ into("${project.buildDir}/generated/source/preprocessed_code")
+ from("${topobjdir}/mobile/android/base/generated/preprocessed")
+}
+
+// The omnijar inputs are listed as resource directory inputs to a dummy JAR.
+// That arrangement labels them nicely in IntelliJ. See the comment in the
+// :omnijar project for more context.
+evaluationDependsOn(':omnijar')
+
+task buildOmnijar(type:Exec) {
+ dependsOn rootProject.generateCodeAndResources
+
+ // See comment in :omnijar project regarding interface mismatches here.
+ inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
+
+ // Produce a single output file.
+ outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
+
+ workingDir "${topobjdir}"
+
+ commandLine mozconfig.substs.GMAKE
+ args '-C'
+ args "${topobjdir}/mobile/android/base"
+ args 'gradle-omnijar'
+
+ // Only show the output if something went wrong.
+ ignoreExitValue = true
+ standardOutput = new ByteArrayOutputStream()
+ errorOutput = standardOutput
+ doLast {
+ if (execResult.exitValue != 0) {
+ throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
+ }
+ }
+}
+
+android.libraryVariants.all { variant ->
+ variant.preBuild.dependsOn syncPreprocessedCode
+
+ // Like 'debug' or 'release'.
+ def buildType = variant.buildType.name
+
+ syncOmnijarFromDistDir.dependsOn buildOmnijar
+ def generateAssetsTask = tasks.findByName("generate${buildType.capitalize()}Assets")
+ generateAssetsTask.dependsOn syncOmnijarFromDistDir
+ generateAssetsTask.dependsOn syncLibsFromDistDir
+ generateAssetsTask.dependsOn syncAssetsFromDistDir
+
+ android.sourceSets."${buildType}".assets.srcDir syncOmnijarFromDistDir.destinationDir
+ android.sourceSets."${buildType}".assets.srcDir syncAssetsFromDistDir.destinationDir
+ android.sourceSets."${buildType}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
+}
copy from mobile/android/base/java/org/mozilla/gecko/annotation/JNITarget.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/JNITarget.java
copy from mobile/android/base/java/org/mozilla/gecko/annotation/ReflectionTarget.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/ReflectionTarget.java
copy from mobile/android/base/java/org/mozilla/gecko/annotation/RobocopTarget.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/RobocopTarget.java
copy from mobile/android/base/java/org/mozilla/gecko/SysInfo.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/SysInfo.java
copy from mobile/android/base/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/WebRTCJNITarget.java
copy from mobile/android/base/java/org/mozilla/gecko/annotation/WrapForJNI.java
copy to mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko/WrapForJNI.java
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.gecko">
+
+</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java
@@ -0,0 +1,147 @@
+/* -*- 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 com.googlecode.eyesfree.braille.selfbraille;
+
+/**
+ * Interface for a client to control braille output for a part of the
+ * accessibility node tree.
+ */
+public interface ISelfBrailleService extends android.os.IInterface {
+ /** Local-side IPC implementation stub class. */
+ public static abstract class Stub extends android.os.Binder implements
+ com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService {
+ private static final java.lang.String DESCRIPTOR = "com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService";
+
+ /** Construct the stub at attach it to the interface. */
+ public Stub() {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+
+ /**
+ * Cast an IBinder object into an
+ * com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService
+ * interface, generating a proxy if needed.
+ */
+ public static com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService asInterface(
+ android.os.IBinder obj) {
+ if ((obj == null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin != null) && (iin instanceof com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService))) {
+ return ((com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService) iin);
+ }
+ return new com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService.Stub.Proxy(
+ obj);
+ }
+
+ @Override
+ public android.os.IBinder asBinder() {
+ return this;
+ }
+
+ @Override
+ public boolean onTransact(int code, android.os.Parcel data,
+ android.os.Parcel reply, int flags)
+ throws android.os.RemoteException {
+ switch (code) {
+ case INTERFACE_TRANSACTION: {
+ reply.writeString(DESCRIPTOR);
+ return true;
+ }
+ case TRANSACTION_write: {
+ data.enforceInterface(DESCRIPTOR);
+ android.os.IBinder _arg0;
+ _arg0 = data.readStrongBinder();
+ com.googlecode.eyesfree.braille.selfbraille.WriteData _arg1;
+ if ((0 != data.readInt())) {
+ _arg1 = com.googlecode.eyesfree.braille.selfbraille.WriteData.CREATOR
+ .createFromParcel(data);
+ } else {
+ _arg1 = null;
+ }
+ this.write(_arg0, _arg1);
+ reply.writeNoException();
+ return true;
+ }
+ case TRANSACTION_disconnect: {
+ data.enforceInterface(DESCRIPTOR);
+ android.os.IBinder _arg0;
+ _arg0 = data.readStrongBinder();
+ this.disconnect(_arg0);
+ return true;
+ }
+ }
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ private static class Proxy implements
+ com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService {
+ private android.os.IBinder mRemote;
+
+ Proxy(android.os.IBinder remote) {
+ mRemote = remote;
+ }
+
+ @Override
+ public android.os.IBinder asBinder() {
+ return mRemote;
+ }
+
+ public java.lang.String getInterfaceDescriptor() {
+ return DESCRIPTOR;
+ }
+
+ @Override
+ public void write(
+ android.os.IBinder clientToken,
+ com.googlecode.eyesfree.braille.selfbraille.WriteData writeData)
+ throws android.os.RemoteException {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStrongBinder(clientToken);
+ if ((writeData != null)) {
+ _data.writeInt(1);
+ writeData.writeToParcel(_data, 0);
+ } else {
+ _data.writeInt(0);
+ }
+ mRemote.transact(Stub.TRANSACTION_write, _data, _reply, 0);
+ _reply.readException();
+ } finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+
+ @Override
+ public void disconnect(android.os.IBinder clientToken)
+ throws android.os.RemoteException {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStrongBinder(clientToken);
+ mRemote.transact(Stub.TRANSACTION_disconnect, _data, null,
+ android.os.IBinder.FLAG_ONEWAY);
+ } finally {
+ _data.recycle();
+ }
+ }
+ }
+
+ static final int TRANSACTION_write = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_disconnect = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ }
+
+ public void write(android.os.IBinder clientToken,
+ com.googlecode.eyesfree.braille.selfbraille.WriteData writeData)
+ throws android.os.RemoteException;
+
+ public void disconnect(android.os.IBinder clientToken)
+ throws android.os.RemoteException;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * 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 com.googlecode.eyesfree.braille.selfbraille;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Client-side interface to the self brailling interface.
+ *
+ * Threading: Instances of this object should be created and shut down
+ * in a thread with a {@link Looper} associated with it. Other methods may
+ * be called on any thread.
+ */
+public class SelfBrailleClient {
+ private static final String LOG_TAG =
+ SelfBrailleClient.class.getSimpleName();
+ private static final String ACTION_SELF_BRAILLE_SERVICE =
+ "com.googlecode.eyesfree.braille.service.ACTION_SELF_BRAILLE_SERVICE";
+ private static final String BRAILLE_BACK_PACKAGE =
+ "com.googlecode.eyesfree.brailleback";
+ private static final Intent mServiceIntent =
+ new Intent(ACTION_SELF_BRAILLE_SERVICE)
+ .setPackage(BRAILLE_BACK_PACKAGE);
+ /**
+ * SHA-1 hash value of the Eyes-Free release key certificate, used to sign
+ * BrailleBack. It was generated from the keystore with:
+ * $ keytool -exportcert -keystore <keystorefile> -alias android.keystore \
+ * > cert
+ * $ keytool -printcert -file cert
+ */
+ // The typecasts are to silence a compiler warning about loss of precision
+ private static final byte[] EYES_FREE_CERT_SHA1 = new byte[] {
+ (byte) 0x9B, (byte) 0x42, (byte) 0x4C, (byte) 0x2D,
+ (byte) 0x27, (byte) 0xAD, (byte) 0x51, (byte) 0xA4,
+ (byte) 0x2A, (byte) 0x33, (byte) 0x7E, (byte) 0x0B,
+ (byte) 0xB6, (byte) 0x99, (byte) 0x1C, (byte) 0x76,
+ (byte) 0xEC, (byte) 0xA4, (byte) 0x44, (byte) 0x61
+ };
+ /**
+ * Delay before the first rebind attempt on bind error or service
+ * disconnect.
+ */
+ private static final int REBIND_DELAY_MILLIS = 500;
+ private static final int MAX_REBIND_ATTEMPTS = 5;
+
+ private final Binder mIdentity = new Binder();
+ private final Context mContext;
+ private final boolean mAllowDebugService;
+ private final SelfBrailleHandler mHandler = new SelfBrailleHandler();
+ private boolean mShutdown = false;
+
+ /**
+ * Written in handler thread, read in any thread calling methods on the
+ * object.
+ */
+ private volatile Connection mConnection;
+ /** Protected by synchronizing on mHandler. */
+ private int mNumFailedBinds = 0;
+
+ /**
+ * Constructs an instance of this class. {@code context} is used to bind
+ * to the self braille service. The current thread must have a Looper
+ * associated with it. If {@code allowDebugService} is true, this instance
+ * will connect to a BrailleBack service without requiring it to be signed
+ * by the release key used to sign BrailleBack.
+ */
+ public SelfBrailleClient(Context context, boolean allowDebugService) {
+ mContext = context;
+ mAllowDebugService = allowDebugService;
+ doBindService();
+ }
+
+ /**
+ * Shuts this instance down, deallocating any global resources it is using.
+ * This method must be called on the same thread that created this object.
+ */
+ public void shutdown() {
+ mShutdown = true;
+ doUnbindService();
+ }
+
+ public void write(WriteData writeData) {
+ writeData.validate();
+ ISelfBrailleService localService = getSelfBrailleService();
+ if (localService != null) {
+ try {
+ localService.write(mIdentity, writeData);
+ } catch (RemoteException ex) {
+ Log.e(LOG_TAG, "Self braille write failed", ex);
+ }
+ }
+ }
+
+ private void doBindService() {
+ Connection localConnection = new Connection();
+ if (!mContext.bindService(mServiceIntent, localConnection,
+ Context.BIND_AUTO_CREATE)) {
+ Log.e(LOG_TAG, "Failed to bind to service");
+ mHandler.scheduleRebind();
+ return;
+ }
+ mConnection = localConnection;
+ Log.i(LOG_TAG, "Bound to self braille service");
+ }
+
+ private void doUnbindService() {
+ if (mConnection != null) {
+ ISelfBrailleService localService = getSelfBrailleService();
+ if (localService != null) {
+ try {
+ localService.disconnect(mIdentity);
+ } catch (RemoteException ex) {
+ // Nothing to do.
+ }
+ }
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ }
+ }
+
+ private ISelfBrailleService getSelfBrailleService() {
+ Connection localConnection = mConnection;
+ if (localConnection != null) {
+ return localConnection.mService;
+ }
+ return null;
+ }
+
+ private boolean verifyPackage() {
+ PackageManager pm = mContext.getPackageManager();
+ PackageInfo pi;
+ try {
+ pi = pm.getPackageInfo(BRAILLE_BACK_PACKAGE,
+ PackageManager.GET_SIGNATURES);
+ } catch (PackageManager.NameNotFoundException ex) {
+ Log.w(LOG_TAG, "Can't verify package " + BRAILLE_BACK_PACKAGE,
+ ex);
+ return false;
+ }
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException ex) {
+ Log.e(LOG_TAG, "SHA-1 not supported", ex);
+ return false;
+ }
+ // Check if any of the certificates match our hash.
+ for (Signature signature : pi.signatures) {
+ digest.update(signature.toByteArray());
+ if (MessageDigest.isEqual(EYES_FREE_CERT_SHA1, digest.digest())) {
+ return true;
+ }
+ digest.reset();
+ }
+ if (mAllowDebugService) {
+ Log.w(LOG_TAG, String.format(
+ "*** %s connected to BrailleBack with invalid (debug?) "
+ + "signature ***",
+ mContext.getPackageName()));
+ return true;
+ }
+ return false;
+ }
+ private class Connection implements ServiceConnection {
+ // Read in application threads, written in main thread.
+ private volatile ISelfBrailleService mService;
+
+ @Override
+ public void onServiceConnected(ComponentName className,
+ IBinder binder) {
+ if (!verifyPackage()) {
+ Log.w(LOG_TAG, String.format("Service certificate mismatch "
+ + "for %s, dropping connection",
+ BRAILLE_BACK_PACKAGE));
+ mHandler.unbindService();
+ return;
+ }
+ Log.i(LOG_TAG, "Connected to self braille service");
+ mService = ISelfBrailleService.Stub.asInterface(binder);
+ synchronized (mHandler) {
+ mNumFailedBinds = 0;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ Log.e(LOG_TAG, "Disconnected from self braille service");
+ mService = null;
+ // Retry by rebinding.
+ mHandler.scheduleRebind();
+ }
+ }
+
+ private class SelfBrailleHandler extends Handler {
+ private static final int MSG_REBIND_SERVICE = 1;
+ private static final int MSG_UNBIND_SERVICE = 2;
+
+ public void scheduleRebind() {
+ synchronized (this) {
+ if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) {
+ int delay = REBIND_DELAY_MILLIS << mNumFailedBinds;
+ sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay);
+ ++mNumFailedBinds;
+ }
+ }
+ }
+
+ public void unbindService() {
+ sendEmptyMessage(MSG_UNBIND_SERVICE);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_REBIND_SERVICE:
+ handleRebindService();
+ break;
+ case MSG_UNBIND_SERVICE:
+ handleUnbindService();
+ break;
+ }
+ }
+
+ private void handleRebindService() {
+ if (mShutdown) {
+ return;
+ }
+ if (mConnection != null) {
+ doUnbindService();
+ }
+ doBindService();
+ }
+
+ private void handleUnbindService() {
+ doUnbindService();
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * 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 com.googlecode.eyesfree.braille.selfbraille;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Represents what should be shown on the braille display for a
+ * part of the accessibility node tree.
+ */
+public class WriteData implements Parcelable {
+
+ private static final String PROP_SELECTION_START = "selectionStart";
+ private static final String PROP_SELECTION_END = "selectionEnd";
+
+ private AccessibilityNodeInfo mAccessibilityNodeInfo;
+ private CharSequence mText;
+ private Bundle mProperties = Bundle.EMPTY;
+
+ /**
+ * Returns a new {@link WriteData} instance for the given {@code view}.
+ */
+ public static WriteData forView(View view) {
+ AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(view);
+ WriteData writeData = new WriteData();
+ writeData.mAccessibilityNodeInfo = node;
+ return writeData;
+ }
+
+ public static WriteData forInfo(AccessibilityNodeInfo info){
+ WriteData writeData = new WriteData();
+ writeData.mAccessibilityNodeInfo = info;
+ return writeData;
+ }
+
+
+ public AccessibilityNodeInfo getAccessibilityNodeInfo() {
+ return mAccessibilityNodeInfo;
+ }
+
+ /**
+ * Sets the text to be displayed when the accessibility node associated
+ * with this instance has focus. If this method is not called (or
+ * {@code text} is {@code null}), this client relinquishes control over
+ * this node.
+ */
+ public WriteData setText(CharSequence text) {
+ mText = text;
+ return this;
+ }
+
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Sets the start position in the text of a text selection or cursor that
+ * should be marked on the display. A negative value (the default) means
+ * no selection will be added.
+ */
+ public WriteData setSelectionStart(int v) {
+ writableProperties().putInt(PROP_SELECTION_START, v);
+ return this;
+ }
+
+ /**
+ * @see {@link #setSelectionStart}.
+ */
+ public int getSelectionStart() {
+ return mProperties.getInt(PROP_SELECTION_START, -1);
+ }
+
+ /**
+ * Sets the end of the text selection to be marked on the display. This
+ * value should only be non-negative if the selection start is
+ * non-negative. If this value is <= the selection start, the selection
+ * is a cursor. Otherwise, the selection covers the range from
+ * start(inclusive) to end (exclusive).
+ *
+ * @see {@link android.text.Selection}.
+ */
+ public WriteData setSelectionEnd(int v) {
+ writableProperties().putInt(PROP_SELECTION_END, v);
+ return this;
+ }
+
+ /**
+ * @see {@link #setSelectionEnd}.
+ */
+ public int getSelectionEnd() {
+ return mProperties.getInt(PROP_SELECTION_END, -1);
+ }
+
+ private Bundle writableProperties() {
+ if (mProperties == Bundle.EMPTY) {
+ mProperties = new Bundle();
+ }
+ return mProperties;
+ }
+
+ /**
+ * Checks constraints on the fields that must be satisfied before sending
+ * this instance to the self braille service.
+ * @throws IllegalStateException
+ */
+ public void validate() throws IllegalStateException {
+ if (mAccessibilityNodeInfo == null) {
+ throw new IllegalStateException(
+ "Accessibility node info can't be null");
+ }
+ int selectionStart = getSelectionStart();
+ int selectionEnd = getSelectionEnd();
+ if (mText == null) {
+ if (selectionStart > 0 || selectionEnd > 0) {
+ throw new IllegalStateException(
+ "Selection can't be set without text");
+ }
+ } else {
+ if (selectionStart < 0 && selectionEnd >= 0) {
+ throw new IllegalStateException(
+ "Selection end without start");
+ }
+ int textLength = mText.length();
+ if (selectionStart > textLength || selectionEnd > textLength) {
+ throw new IllegalStateException("Selection out of bounds");
+ }
+ }
+ }
+
+ // For Parcelable support.
+
+ public static final Parcelable.Creator<WriteData> CREATOR =
+ new Parcelable.Creator<WriteData>() {
+ @Override
+ public WriteData createFromParcel(Parcel in) {
+ return new WriteData(in);
+ }
+
+ @Override
+ public WriteData[] newArray(int size) {
+ return new WriteData[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <strong>Note:</strong> The {@link AccessibilityNodeInfo} will be
+ * recycled by this method, don't try to use this more than once.
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mAccessibilityNodeInfo.writeToParcel(out, flags);
+ // The above call recycles the node, so make sure we don't use it
+ // anymore.
+ mAccessibilityNodeInfo = null;
+ out.writeString(mText.toString());
+ out.writeBundle(mProperties);
+ }
+
+ private WriteData() {
+ }
+
+ private WriteData(Parcel in) {
+ mAccessibilityNodeInfo =
+ AccessibilityNodeInfo.CREATOR.createFromParcel(in);
+ mText = in.readString();
+ mProperties = in.readBundle();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ANRReporter.java
@@ -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);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java
@@ -0,0 +1,42 @@
+/* -*- 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 android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class AlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ final WakeLock wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "GeckoAlarm");
+ wakeLock.acquire();
+
+ AlarmReceiver.notifyAlarmFired();
+ TimerTask releaseLockTask = new TimerTask() {
+ @Override
+ public void run() {
+ wakeLock.release();
+ }
+ };
+ Timer timer = new Timer();
+ // 5 seconds ought to be enough for anybody
+ timer.schedule(releaseLockTask, 5 * 1000);
+ }
+
+ @WrapForJNI
+ private static native void notifyAlarmFired();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
@@ -0,0 +1,392 @@
+/* -*- 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.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+
+public class AndroidGamepadManager {
+ // This is completely arbitrary.
+ private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
+ private static final long POLL_TIMER_PERIOD = 1000; // milliseconds
+
+ private static enum Axis {
+ X(MotionEvent.AXIS_X),
+ Y(MotionEvent.AXIS_Y),
+ Z(MotionEvent.AXIS_Z),
+ RZ(MotionEvent.AXIS_RZ);
+
+ public final int axis;
+
+ private Axis(int axis) {
+ this.axis = axis;
+ }
+ }
+
+ // A list of gamepad button mappings. Axes are determined at
+ // runtime, as they vary by Android version.
+ private static enum Trigger {
+ Left(6),
+ Right(7);
+
+ public final int button;
+
+ private Trigger(int button) {
+ this.button = button;
+ }
+ }
+
+ private static final int FIRST_DPAD_BUTTON = 12;
+ // A list of axis number, gamepad button mappings for negative, positive.
+ // Button mappings are added to FIRST_DPAD_BUTTON.
+ private static enum DpadAxis {
+ UpDown(MotionEvent.AXIS_HAT_Y, 0, 1),
+ LeftRight(MotionEvent.AXIS_HAT_X, 2, 3);
+
+ public final int axis;
+ public final int negativeButton;
+ public final int positiveButton;
+
+ private DpadAxis(int axis, int negativeButton, int positiveButton) {
+ this.axis = axis;
+ this.negativeButton = negativeButton;
+ this.positiveButton = positiveButton;
+ }
+ }
+
+ private static enum Button {
+ A(KeyEvent.KEYCODE_BUTTON_A),
+ B(KeyEvent.KEYCODE_BUTTON_B),
+ X(KeyEvent.KEYCODE_BUTTON_X),
+ Y(KeyEvent.KEYCODE_BUTTON_Y),
+ L1(KeyEvent.KEYCODE_BUTTON_L1),
+ R1(KeyEvent.KEYCODE_BUTTON_R1),
+ L2(KeyEvent.KEYCODE_BUTTON_L2),
+ R2(KeyEvent.KEYCODE_BUTTON_R2),
+ SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
+ START(KeyEvent.KEYCODE_BUTTON_START),
+ THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
+ THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
+ DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
+ DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
+ DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
+ DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT);
+
+ public final int button;
+
+ private Button(int button) {
+ this.button = button;
+ }
+ }
+
+ private static class Gamepad {
+ // ID from GamepadService
+ public int id;
+ // Retain axis state so we can determine changes.
+ public float axes[];
+ public boolean dpad[];
+ public int triggerAxes[];
+ public float triggers[];
+
+ public Gamepad(int serviceId, int deviceId) {
+ id = serviceId;
+ axes = new float[Axis.values().length];
+ dpad = new boolean[4];
+ triggers = new float[2];
+
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device != null) {
+ // LTRIGGER/RTRIGGER don't seem to be exposed on older
+ // versions of Android.
+ if (device.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null && device.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null) {
+ triggerAxes = new int[]{MotionEvent.AXIS_LTRIGGER,
+ MotionEvent.AXIS_RTRIGGER};
+ } else if (device.getMotionRange(MotionEvent.AXIS_BRAKE) != null && device.getMotionRange(MotionEvent.AXIS_GAS) != null) {
+ triggerAxes = new int[]{MotionEvent.AXIS_BRAKE,
+ MotionEvent.AXIS_GAS};
+ } else {
+ triggerAxes = null;
+ }
+ }
+ }
+ }
+
+ private static boolean sStarted;
+ private static final SparseArray<Gamepad> sGamepads = new SparseArray<>();
+ private static final SparseArray<List<KeyEvent>> sPendingGamepads = new SparseArray<>();
+ private static InputManager.InputDeviceListener sListener;
+ private static Timer sPollTimer;
+
+ private AndroidGamepadManager() {
+ }
+
+ public static void startup() {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ scanForGamepads();
+ addDeviceListener();
+ sStarted = true;
+ }
+ }
+
+ public static void shutdown() {
+ ThreadUtils.assertOnUiThread();
+ if (sStarted) {
+ removeDeviceListener();
+ sPendingGamepads.clear();
+ sGamepads.clear();
+ sStarted = false;
+ }
+ }
+
+ public static void gamepadAdded(int deviceId, int serviceId) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return;
+ }
+
+ final List<KeyEvent> pending = sPendingGamepads.get(deviceId);
+ if (pending == null) {
+ removeGamepad(deviceId);
+ return;
+ }
+
+ sPendingGamepads.remove(deviceId);
+ sGamepads.put(deviceId, new Gamepad(serviceId, deviceId));
+ // Handle queued KeyEvents
+ for (KeyEvent ev : pending) {
+ handleKeyEvent(ev);
+ }
+ }
+
+ private static float deadZone(MotionEvent ev, int axis) {
+ if (GamepadUtils.isValueInDeadZone(ev, axis)) {
+ return 0.0f;
+ }
+ return ev.getAxisValue(axis);
+ }
+
+ private static void mapDpadAxis(Gamepad gamepad,
+ boolean pressed,
+ float value,
+ int which) {
+ if (pressed != gamepad.dpad[which]) {
+ gamepad.dpad[which] = pressed;
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value)));
+ }
+ }
+
+ public static boolean handleMotionEvent(MotionEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ final Gamepad gamepad = sGamepads.get(ev.getDeviceId());
+ if (gamepad == null) {
+ // Not a device we care about.
+ return false;
+ }
+
+ // First check the analog stick axes
+ boolean[] valid = new boolean[Axis.values().length];
+ float[] axes = new float[Axis.values().length];
+ boolean anyValidAxes = false;
+ for (Axis axis : Axis.values()) {
+ float value = deadZone(ev, axis.axis);
+ int i = axis.ordinal();
+ if (value != gamepad.axes[i]) {
+ axes[i] = value;
+ gamepad.axes[i] = value;
+ valid[i] = true;
+ anyValidAxes = true;
+ }
+ }
+ if (anyValidAxes) {
+ // Send an axismove event.
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAxisEvent(gamepad.id, valid, axes));
+ }
+
+ // Map triggers to buttons.
+ if (gamepad.triggerAxes != null) {
+ for (Trigger trigger : Trigger.values()) {
+ int i = trigger.ordinal();
+ int axis = gamepad.triggerAxes[i];
+ float value = deadZone(ev, axis);
+ if (value != gamepad.triggers[i]) {
+ gamepad.triggers[i] = value;
+ boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, trigger.button, pressed, value));
+ }
+ }
+ }
+ // Map d-pad to buttons.
+ for (DpadAxis dpadaxis : DpadAxis.values()) {
+ float value = deadZone(ev, dpadaxis.axis);
+ mapDpadAxis(gamepad, value < 0.0f, value, dpadaxis.negativeButton);
+ mapDpadAxis(gamepad, value > 0.0f, value, dpadaxis.positiveButton);
+ }
+ return true;
+ }
+
+ public static boolean handleKeyEvent(KeyEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ int deviceId = ev.getDeviceId();
+ final List<KeyEvent> pendingGamepad = sPendingGamepads.get(deviceId);
+ if (pendingGamepad != null) {
+ // Queue up key events for pending devices.
+ pendingGamepad.add(ev);
+ return true;
+ }
+
+ if (sGamepads.get(deviceId) == null) {
+ InputDevice device = ev.getDevice();
+ if (device != null &&
+ (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ // This is a gamepad we haven't seen yet.
+ addGamepad(device);
+ sPendingGamepads.get(deviceId).add(ev);
+ return true;
+ }
+ // Not a device we care about.
+ return false;
+ }
+
+ int key = -1;
+ for (Button button : Button.values()) {
+ if (button.button == ev.getKeyCode()) {
+ key = button.ordinal();
+ break;
+ }
+ }
+ if (key == -1) {
+ // Not a key we know how to handle.
+ return false;
+ }
+ if (ev.getRepeatCount() > 0) {
+ // We would handle this key, but we're not interested in
+ // repeats. Eat it.
+ return true;
+ }
+
+ Gamepad gamepad = sGamepads.get(deviceId);
+ boolean pressed = ev.getAction() == KeyEvent.ACTION_DOWN;
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, key, pressed, pressed ? 1.0f : 0.0f));
+ return true;
+ }
+
+ private static void scanForGamepads() {
+ int[] deviceIds = InputDevice.getDeviceIds();
+ if (deviceIds == null) {
+ return;
+ }
+ for (int i = 0; i < deviceIds.length; i++) {
+ InputDevice device = InputDevice.getDevice(deviceIds[i]);
+ if (device == null) {
+ continue;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
+ continue;
+ }
+ addGamepad(device);
+ }
+ }
+
+ private static void addGamepad(InputDevice device) {
+ //TODO: when we're using a newer SDK version, use these.
+ //if (Build.VERSION.SDK_INT >= 12) {
+ //int vid = device.getVendorId();
+ //int pid = device.getProductId();
+ //}
+ sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(device.getId(), true));
+ }
+
+ private static void removeGamepad(int deviceId) {
+ Gamepad gamepad = sGamepads.get(deviceId);
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(gamepad.id, false));
+ sGamepads.remove(deviceId);
+ }
+
+ private static void addDeviceListener() {
+ if (Versions.preJB) {
+ // Poll known gamepads to see if they've disappeared.
+ sPollTimer = new Timer();
+ sPollTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ for (int i = 0; i < sGamepads.size(); ++i) {
+ final int deviceId = sGamepads.keyAt(i);
+ if (InputDevice.getDevice(deviceId) == null) {
+ removeGamepad(deviceId);
+ }
+ }
+ }
+ }, POLL_TIMER_PERIOD, POLL_TIMER_PERIOD);
+ return;
+ }
+ sListener = new InputManager.InputDeviceListener() {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device == null) {
+ return;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ addGamepad(device);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ if (sPendingGamepads.get(deviceId) != null) {
+ // Got removed before Gecko's ack reached us.
+ // gamepadAdded will deal with it.
+ sPendingGamepads.remove(deviceId);
+ return;
+ }
+ if (sGamepads.get(deviceId) != null) {
+ removeGamepad(deviceId);
+ }
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ }
+ };
+ ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).registerInputDeviceListener(sListener, ThreadUtils.getUiHandler());
+ }
+
+ private static void removeDeviceListener() {
+ if (Versions.preJB) {
+ if (sPollTimer != null) {
+ sPollTimer.cancel();
+ sPollTimer = null;
+ }
+ return;
+ }
+ ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).unregisterInputDeviceListener(sListener);
+ sListener = null;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java
@@ -0,0 +1,186 @@
+/* -*- 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.ActivityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.RectF;
+import android.hardware.SensorEventListener;
+import android.location.LocationListener;
+import android.view.View;
+import android.widget.AbsoluteLayout;
+
+public class BaseGeckoInterface implements GeckoAppShell.GeckoInterface {
+ // Bug 908744: Implement GeckoEventListener
+ // Bug 908752: Implement SensorEventListener
+ // Bug 908755: Implement LocationListener
+ // Bug 908756: Implement Tabs.OnTabsChangedListener
+ // Bug 908760: Implement GeckoEventResponder
+
+ private final Context mContext;
+ private GeckoProfile mProfile;
+
+ public BaseGeckoInterface(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public GeckoProfile getProfile() {
+ // Fall back to default profile if we didn't load a specific one
+ if (mProfile == null) {
+ mProfile = GeckoProfile.get(mContext);
+ }
+ return mProfile;
+ }
+
+ @Override
+ public Activity getActivity() {
+ return (Activity)mContext;
+ }
+
+ @Override
+ public String getDefaultUAString() {
+ return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
+ AppConstants.USER_AGENT_FENNEC_MOBILE;
+ }
+
+ // Bug 908772: Implement this
+ @Override
+ public LocationListener getLocationListener() {
+ return null;
+ }
+
+ // Bug 908773: Implement this
+ @Override
+ public SensorEventListener getSensorEventListener() {
+ return null;
+ }
+
+ // Bug 908775: Implement this
+ @Override
+ public void doRestart() {}
+
+ @Override
+ public void setFullScreen(final boolean fullscreen) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ActivityUtils.setFullScreen(getActivity(), fullscreen);
+ }
+ });
+ }
+
+ // Bug 908779: Implement this
+ @Override
+ public void addPluginView(final View view, final RectF rect, final boolean isFullScreen) {}
+
+ // Bug 908781: Implement this
+ @Override
+ public void removePluginView(final View view, final boolean isFullScreen) {}
+
+ // Bug 908783: Implement this
+ @Override
+ public void enableCameraView() {}
+
+ // Bug 908785: Implement this
+ @Override
+ public void disableCameraView() {}
+
+ // Bug 908786: Implement this
+ @Override
+ public void addAppStateListener(GeckoAppShell.AppStateListener listener) {}
+
+ // Bug 908787: Implement this
+ @Override
+ public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {}
+
+ // Bug 908788: Implement this
+ @Override
+ public View getCameraView() {
+ return null;
+ }
+
+ // Bug 908789: Implement this
+ @Override
+ public void notifyWakeLockChanged(String topic, String state) {}
+
+ // Bug 908790: Implement this
+ @Override
+ public void onInputMethodChanged(String inputMethod) {}
+
+ @Override
+ public boolean areTabsShown() {
+ return false;
+ }
+
+ // Bug 908791: Implement this
+ @Override
+ public AbsoluteLayout getPluginContainer() {
+ return null;
+ }
+
+ @Override
+ public void notifyCheckUpdateResult(String result) {
+ GeckoAppShell.notifyObservers("Update:CheckResult", result);
+ }
+
+ // Bug 908792: Implement this
+ @Override
+ public void invalidateOptionsMenu() {}
+
+ @Override
+ public void createShortcut(String title, String URI) {
+ // By default, do nothing.
+ }
+
+ @Override
+ public void checkUriVisited(String uri) {
+ // By default, no URIs are considered visited.
+ }
+
+ @Override
+ public void markUriVisited(final String uri) {
+ // By default, no URIs are marked as visited.
+ }
+
+ @Override
+ public void setUriTitle(final String uri, final String title) {
+ // By default, no titles are associated with URIs.
+ }
+
+ @Override
+ public void setAccessibilityEnabled(boolean enabled) {
+ // By default, take no action when accessibility is toggled on or off.
+ }
+
+ @Override
+ public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title) {
+ // By default, never open external URIs.
+ return false;
+ }
+
+ @Override
+ public String[] getHandlersForMimeType(String mimeType, String action) {
+ // By default, offer no handlers for any MIME type.
+ return new String[] {};
+ }
+
+ @Override
+ public String[] getHandlersForURL(String url, String action) {
+ // By default, offer no handlers for any URL.
+ return new String[] {};
+ }
+
+ @Override
+ public String getDefaultChromeURI() {
+ // By default, use the GeckoView-specific chrome URI.
+ return "chrome://browser/content/geckoview.xul";
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java
@@ -0,0 +1,15 @@
+/* -*- 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.content.Context;
+import android.content.SharedPreferences;
+
+public interface ContextGetter {
+ Context getContext();
+ SharedPreferences getSharedPreferences();
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java
@@ -0,0 +1,469 @@
+/* -*- 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.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.UUID;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.util.Log;
+
+public class CrashHandler implements Thread.UncaughtExceptionHandler {
+
+ private static final String LOGTAG = "GeckoCrashHandler";
+ private static final Thread MAIN_THREAD = Thread.currentThread();
+ private static final String DEFAULT_SERVER_URL =
+ "https://crash-reports.mozilla.com/submit?id=%1$s&version=%2$s&buildid=%3$s";
+
+ // Context for getting device information
+ protected final Context appContext;
+ // Thread that this handler applies to, or null for a global handler
+ protected final Thread handlerThread;
+ protected final Thread.UncaughtExceptionHandler systemUncaughtHandler;
+
+ protected boolean crashing;
+ protected boolean unregistered;
+
+ /**
+ * Get the root exception from the 'cause' chain of an exception.
+ *
+ * @param exc An exception
+ * @return The root exception
+ */
+ public static Throwable getRootException(Throwable exc) {
+ for (Throwable cause = exc; cause != null; cause = cause.getCause()) {
+ exc = cause;
+ }
+ return exc;
+ }
+
+ /**
+ * Get the standard stack trace string of an exception.
+ *
+ * @param exc An exception
+ * @return The exception stack trace.
+ */
+ public static String getExceptionStackTrace(final Throwable exc) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ exc.printStackTrace(pw);
+ pw.flush();
+ return sw.toString();
+ }
+
+ /**
+ * Terminate the current process.
+ */
+ public static void terminateProcess() {
+ Process.killProcess(Process.myPid());
+ }
+
+ /**
+ * Create and register a CrashHandler for all threads and thread groups.
+ */
+ public CrashHandler() {
+ this((Context) null);
+ }
+
+ /**
+ * Create and register a CrashHandler for all threads and thread groups.
+ *
+ * @param appContext A Context for retrieving application information.
+ */
+ public CrashHandler(final Context appContext) {
+ this.appContext = appContext;
+ this.handlerThread = null;
+ this.systemUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();
+ Thread.setDefaultUncaughtExceptionHandler(this);
+ }
+
+ /**
+ * Create and register a CrashHandler for a particular thread.
+ *
+ * @param thread A thread to register the CrashHandler
+ */
+ public CrashHandler(final Thread thread) {
+ this(thread, null);
+ }
+
+ /**
+ * Create and register a CrashHandler for a particular thread.
+ *
+ * @param thread A thread to register the CrashHandler
+ * @param appContext A Context for retrieving application information.
+ */
+ public CrashHandler(final Thread thread, final Context appContext) {
+ this.appContext = appContext;
+ this.handlerThread = thread;
+ this.systemUncaughtHandler = thread.getUncaughtExceptionHandler();
+ thread.setUncaughtExceptionHandler(this);
+ }
+
+ /**
+ * Unregister this CrashHandler for exception handling.
+ */
+ public void unregister() {
+ unregistered = true;
+
+ // Restore the previous handler if we are still the topmost handler.
+ // If not, we are part of a chain of handlers, and we cannot just restore the previous
+ // handler, because that would replace whatever handler that's above us in the chain.
+
+ if (handlerThread != null) {
+ if (handlerThread.getUncaughtExceptionHandler() == this) {
+ handlerThread.setUncaughtExceptionHandler(systemUncaughtHandler);
+ }
+ } else {
+ if (Thread.getDefaultUncaughtExceptionHandler() == this) {
+ Thread.setDefaultUncaughtExceptionHandler(systemUncaughtHandler);
+ }
+ }
+ }
+
+ /**
+ * Record an exception stack in logs.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ */
+ public static void logException(final Thread thread, final Throwable exc) {
+ try {
+ Log.e(LOGTAG, ">>> REPORTING UNCAUGHT EXCEPTION FROM THREAD "
+ + thread.getId() + " (\"" + thread.getName() + "\")", exc);
+
+ if (MAIN_THREAD != thread) {
+ Log.e(LOGTAG, "Main thread (" + MAIN_THREAD.getId() + ") stack:");
+ for (StackTraceElement ste : MAIN_THREAD.getStackTrace()) {
+ Log.e(LOGTAG, " " + ste.toString());
+ }
+ }
+ } catch (final Throwable e) {
+ // If something throws here, we want to continue to report the exception,
+ // so we catch all exceptions and ignore them.
+ }
+ }
+
+ private static long getCrashTime() {
+ return System.currentTimeMillis() / 1000;
+ }
+
+ private static long getStartupTime() {
+ // Process start time is also the proc file modified time.
+ final long uptimeMins = (new File("/proc/self/cmdline")).lastModified();
+ if (uptimeMins == 0L) {
+ return getCrashTime();
+ }
+ return uptimeMins / 1000;
+ }
+
+ private static String getJavaPackageName() {
+ return CrashHandler.class.getPackage().getName();
+ }
+
+ protected String getAppPackageName() {
+ final Context context = getAppContext();
+
+ if (context != null) {
+ return context.getPackageName();
+ }
+
+ try {
+ // Package name is also the command line string in most cases.
+ final FileReader reader = new FileReader("/proc/self/cmdline");
+ final char[] buffer = new char[64];
+ try {
+ if (reader.read(buffer) > 0) {
+ // cmdline is delimited by '\0', and we want the first token.
+ final int nul = Arrays.asList(buffer).indexOf('\0');
+ return (new String(buffer, 0, nul < 0 ? buffer.length : nul)).trim();
+ }
+ } finally {
+ reader.close();
+ }
+
+ } catch (final IOException e) {
+ Log.i(LOGTAG, "Error reading package name", e);
+ }
+
+ // Fallback to using CrashHandler's package name.
+ return getJavaPackageName();
+ }
+
+ protected Context getAppContext() {
+ return appContext;
+ }
+
+ /**
+ * Get the crash "extras" to be reported.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return "Extras" in the from of a Bundle
+ */
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Context context = getAppContext();
+ final Bundle extras = new Bundle();
+ final String pkgName = getAppPackageName();
+
+ extras.putString("ProductName", pkgName);
+ extras.putLong("CrashTime", getCrashTime());
+ extras.putLong("StartupTime", getStartupTime());
+
+ if (context != null) {
+ final PackageManager pkgMgr = context.getPackageManager();
+ try {
+ final PackageInfo pkgInfo = pkgMgr.getPackageInfo(pkgName, 0);
+ extras.putString("Version", pkgInfo.versionName);
+ extras.putInt("BuildID", pkgInfo.versionCode);
+ extras.putLong("InstallTime", pkgInfo.lastUpdateTime / 1000);
+ } catch (final PackageManager.NameNotFoundException e) {
+ Log.i(LOGTAG, "Error getting package info", e);
+ }
+ }
+
+ extras.putString("JavaStackTrace", getExceptionStackTrace(exc));
+ return extras;
+ }
+
+ /**
+ * Get the crash minidump content to be reported.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return Minidump content
+ */
+ protected byte[] getCrashDump(final Thread thread, final Throwable exc) {
+ return new byte[0]; // No minidump.
+ }
+
+ protected static String normalizeUrlString(final String str) {
+ if (str == null) {
+ return "";
+ }
+ return Uri.encode(str);
+ }
+
+ /**
+ * Get the server URL to send the crash report to.
+ *
+ * @param extras The crash extras Bundle
+ */
+ protected String getServerUrl(final Bundle extras) {
+ return String.format(DEFAULT_SERVER_URL,
+ normalizeUrlString(extras.getString("ProductID")),
+ normalizeUrlString(extras.getString("Version")),
+ normalizeUrlString(extras.getString("BuildID")));
+ }
+
+ /**
+ * Launch the crash reporter activity that sends the crash report to the server.
+ *
+ * @param dumpFile Path for the minidump file
+ * @param extraFile Path for the crash extra file
+ * @return Whether the crash reporter was successfully launched
+ */
+ protected boolean launchCrashReporter(final String dumpFile, final String extraFile) {
+ try {
+ final Context context = getAppContext();
+ final String javaPkg = getJavaPackageName();
+ final String pkg = getAppPackageName();
+ final String component = javaPkg + ".CrashReporter";
+ final String action = javaPkg + ".reportCrash";
+ final ProcessBuilder pb;
+
+ if (context != null) {
+ final Intent intent = new Intent(action);
+ intent.setComponent(new ComponentName(pkg, component));
+ intent.putExtra("minidumpPath", dumpFile);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ return true;
+ }
+
+ // Avoid AppConstants dependency for SDK version constants,
+ // because CrashHandler could be used outside of Fennec code.
+ if (Build.VERSION.SDK_INT < 17) {
+ pb = new ProcessBuilder(
+ "/system/bin/am", "start",
+ "-a", action,
+ "-n", pkg + '/' + component,
+ "--es", "minidumpPath", dumpFile);
+ } else {
+ pb = new ProcessBuilder(
+ "/system/bin/am", "start",
+ "--user", /* USER_CURRENT_OR_SELF */ "-3",
+ "-a", action,
+ "-n", pkg + '/' + component,
+ "--es", "minidumpPath", dumpFile);
+ }
+
+ pb.start().waitFor();
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error launching crash reporter", e);
+ return false;
+
+ } catch (final InterruptedException e) {
+ Log.i(LOGTAG, "Interrupted while waiting to launch crash reporter", e);
+ // Fall-through
+ }
+ return true;
+ }
+
+ /**
+ * Report an exception to Socorro.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return Whether the exception was successfully reported
+ */
+ protected boolean reportException(final Thread thread, final Throwable exc) {
+ final Context context = getAppContext();
+ final String id = UUID.randomUUID().toString();
+
+ // Use the cache directory under the app directory to store crash files.
+ final File dir;
+ if (context != null) {
+ dir = context.getCacheDir();
+ } else {
+ dir = new File("/data/data/" + getAppPackageName() + "/cache");
+ }
+
+ dir.mkdirs();
+ if (!dir.exists()) {
+ return false;
+ }
+
+ final File dmpFile = new File(dir, id + ".dmp");
+ final File extraFile = new File(dir, id + ".extra");
+
+ try {
+ // Write out minidump file as binary.
+
+ final byte[] minidump = getCrashDump(thread, exc);
+ final FileOutputStream dmpStream = new FileOutputStream(dmpFile);
+ try {
+ dmpStream.write(minidump);
+ } finally {
+ dmpStream.close();
+ }
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error writing minidump file", e);
+ return false;
+ }
+
+ try {
+ // Write out crash extra file as text.
+
+ final Bundle extras = getCrashExtras(thread, exc);
+ final String url = getServerUrl(extras);
+ extras.putString("ServerURL", url);
+
+ final BufferedWriter extraWriter = new BufferedWriter(new FileWriter(extraFile));
+ try {
+ for (String key : extras.keySet()) {
+ // Each extra line is in the format, key=value, with newlines escaped.
+ extraWriter.write(key);
+ extraWriter.write('=');
+ extraWriter.write(String.valueOf(extras.get(key)).replace("\n", "\\n"));
+ extraWriter.write('\n');
+ }
+ } finally {
+ extraWriter.close();
+ }
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error writing extra file", e);
+ return false;
+ }
+
+ return launchCrashReporter(dmpFile.getAbsolutePath(), extraFile.getAbsolutePath());
+ }
+
+ /**
+ * Implements the default behavior for handling uncaught exceptions.
+ *
+ * @param thread The exception thread
+ * @param exc An uncaught exception
+ */
+ @Override
+ public void uncaughtException(Thread thread, Throwable exc) {
+ if (this.crashing) {
+ // Prevent possible infinite recusions.
+ return;
+ }
+
+ if (thread == null) {
+ // Gecko may pass in null for thread to denote the current thread.
+ thread = Thread.currentThread();
+ }
+
+ try {
+ if (!this.unregistered) {
+ // Only process crash ourselves if we have not been unregistered.
+
+ this.crashing = true;
+ exc = getRootException(exc);
+ logException(thread, exc);
+
+ if (reportException(thread, exc)) {
+ // Reporting succeeded; we can terminate our process now.
+ return;
+ }
+ }
+
+ if (systemUncaughtHandler != null) {
+ // Follow the chain of uncaught handlers.
+ systemUncaughtHandler.uncaughtException(thread, exc);
+ }
+ } finally {
+ terminateProcess();
+ }
+ }
+
+ public static CrashHandler createDefaultCrashHandler(final Context context) {
+ return new CrashHandler(context) {
+ @Override
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Bundle extras = super.getCrashExtras(thread, exc);
+
+ extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
+ extras.putString("ProductID", AppConstants.MOZ_APP_ID);
+ extras.putString("Version", AppConstants.MOZ_APP_VERSION);
+ extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
+ extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
+ extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
+ return extras;
+ }
+
+ @Override
+ public boolean reportException(final Thread thread, final Throwable exc) {
+ if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
+ // Only use Java crash reporter if enabled on official build.
+ return super.reportException(thread, exc);
+ }
+ return false;
+ }
+ };
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
@@ -0,0 +1,409 @@
+/* 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.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSContainer;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@RobocopTarget
+public final class EventDispatcher {
+ private static final String LOGTAG = "GeckoEventDispatcher";
+ private static final String GUID = "__guid__";
+ private static final String STATUS_ERROR = "error";
+ private static final String STATUS_SUCCESS = "success";
+
+ private static final EventDispatcher INSTANCE = new EventDispatcher();
+
+ /**
+ * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size
+ * of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to
+ * empirically determine the initial capacity that avoids rehashing, we need to
+ * determine the initial size, divide it by 75%, and round up to the next power-of-2.
+ */
+ private static final int DEFAULT_GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap
+ private static final int DEFAULT_GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured
+ private static final int DEFAULT_UI_EVENTS_COUNT = 0; // Default for HashMap
+ private static final int DEFAULT_BACKGROUND_EVENTS_COUNT = 0; // Default for HashMap
+
+ private final Map<String, List<NativeEventListener>> mGeckoThreadNativeListeners =
+ new HashMap<String, List<NativeEventListener>>(DEFAULT_GECKO_NATIVE_EVENTS_COUNT);
+ private final Map<String, List<GeckoEventListener>> mGeckoThreadJSONListeners =
+ new HashMap<String, List<GeckoEventListener>>(DEFAULT_GECKO_JSON_EVENTS_COUNT);
+ private final Map<String, List<BundleEventListener>> mUiThreadListeners =
+ new HashMap<String, List<BundleEventListener>>(DEFAULT_UI_EVENTS_COUNT);
+ private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners =
+ new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT);
+
+ public static EventDispatcher getInstance() {
+ return INSTANCE;
+ }
+
+ private EventDispatcher() {
+ }
+
+ private <T> void registerListener(final Class<?> listType,
+ final Map<String, List<T>> listenersMap,
+ final T listener,
+ final String[] events) {
+ try {
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ List<T> listeners = listenersMap.get(event);
+ if (listeners == null) {
+ // Java doesn't let us put Class<? extends List<T>> as the type for listType.
+ @SuppressWarnings("unchecked")
+ final Class<? extends List<T>> type = (Class) listType;
+ listeners = type.newInstance();
+ listenersMap.put(event, listeners);
+ }
+ if (!AppConstants.RELEASE_BUILD && listeners.contains(listener)) {
+ throw new IllegalStateException("Already registered " + event);
+ }
+ listeners.add(listener);
+ }
+ }
+ } catch (final IllegalAccessException | InstantiationException e) {
+ throw new IllegalArgumentException("Invalid new list type", e);
+ }
+ }
+
+ private void checkNotRegisteredElsewhere(final Map<String, ?> allowedMap,
+ final String[] events) {
+ if (AppConstants.RELEASE_BUILD) {
+ // for performance reasons, we only check for
+ // already-registered listeners in non-release builds.
+ return;
+ }
+ for (final Map<String, ?> listenersMap : Arrays.asList(mGeckoThreadNativeListeners,
+ mGeckoThreadJSONListeners,
+ mUiThreadListeners,
+ mBackgroundThreadListeners)) {
+ if (listenersMap == allowedMap) {
+ continue;
+ }
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ if (listenersMap.get(event) != null) {
+ throw new IllegalStateException(
+ "Already registered " + event + " under a different type");
+ }
+ }
+ }
+ }
+ }
+
+ private <T> void unregisterListener(final Map<String, List<T>> listenersMap,
+ final T listener,
+ final String[] events) {
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ List<T> listeners = listenersMap.get(event);
+ if ((listeners == null ||
+ !listeners.remove(listener)) && !AppConstants.RELEASE_BUILD) {
+ throw new IllegalArgumentException(event + " was not registered");
+ }
+ }
+ }
+ }
+
+ public void registerGeckoThreadListener(final NativeEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mGeckoThreadNativeListeners, events);
+
+ // For listeners running on the Gecko thread, we want to notify the listeners
+ // outside of our synchronized block, because the listeners may take an
+ // indeterminate amount of time to run. Therefore, to ensure concurrency when
+ // iterating the list outside of the synchronized block, we use a
+ // CopyOnWriteArrayList.
+ registerListener(CopyOnWriteArrayList.class,
+ mGeckoThreadNativeListeners, listener, events);
+ }
+
+ @Deprecated // Use NativeEventListener instead
+ public void registerGeckoThreadListener(final GeckoEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mGeckoThreadJSONListeners, events);
+
+ registerListener(CopyOnWriteArrayList.class,
+ mGeckoThreadJSONListeners, listener, events);
+ }
+
+ public void registerUiThreadListener(final BundleEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mUiThreadListeners, events);
+
+ registerListener(ArrayList.class,
+ mUiThreadListeners, listener, events);
+ }
+
+ public void registerBackgroundThreadListener(final BundleEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mBackgroundThreadListeners, events);
+
+ registerListener(ArrayList.class,
+ mBackgroundThreadListeners, listener, events);
+ }
+
+ public void unregisterGeckoThreadListener(final NativeEventListener listener,
+ final String... events) {
+ unregisterListener(mGeckoThreadNativeListeners, listener, events);
+ }
+
+ @Deprecated // Use NativeEventListener instead
+ public void unregisterGeckoThreadListener(final GeckoEventListener listener,
+ final String... events) {
+ unregisterListener(mGeckoThreadJSONListeners, listener, events);
+ }
+
+ public void unregisterUiThreadListener(final BundleEventListener listener,
+ final String... events) {
+ unregisterListener(mUiThreadListeners, listener, events);
+ }
+
+ public void unregisterBackgroundThreadListener(final BundleEventListener listener,
+ final String... events) {
+ unregisterListener(mBackgroundThreadListeners, listener, events);
+ }
+
+ public void dispatchEvent(final NativeJSContainer message) {
+ // First try native listeners.
+ final String type = message.optString("type", null);
+ if (type == null) {
+ Log.e(LOGTAG, "JSON message must have a type property");
+ return;
+ }
+
+ final List<NativeEventListener> listeners;
+ synchronized (mGeckoThreadNativeListeners) {
+ listeners = mGeckoThreadNativeListeners.get(type);
+ }
+
+ final String guid = message.optString(GUID, null);
+ EventCallback callback = null;
+ if (guid != null) {
+ callback = new GeckoEventCallback(guid, type);
+ }
+
+ if (listeners != null) {
+ if (listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type);
+
+ // There were native listeners, and they're gone. Dispatch an error rather than
+ // looking for JSON listeners.
+ if (callback != null) {
+ callback.sendError("No listeners for request");
+ }
+ }
+ try {
+ for (final NativeEventListener listener : listeners) {
+ listener.handleMessage(type, message, callback);
+ }
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ Log.e(LOGTAG, "Exception occurred while handling " + type, e);
+ }
+ // If we found native listeners, we assume we don't have any other types of listeners
+ // and return early. This assumption is checked when registering listeners.
+ return;
+ }
+
+ // Check for thread event listeners before checking for JSON event listeners,
+ // because checking for thread listeners is very fast and doesn't require us to
+ // serialize into JSON and construct a JSONObject.
+ if (dispatchToThread(type, message, callback,
+ mUiThreadListeners, ThreadUtils.getUiHandler()) ||
+ dispatchToThread(type, message, callback,
+ mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) {
+
+ // If we found thread listeners, we assume we don't have any other types of listeners
+ // and return early. This assumption is checked when registering listeners.
+ return;
+ }
+
+ try {
+ // If we didn't find native listeners, try JSON listeners.
+ dispatchEvent(new JSONObject(message.toString()), callback);
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Cannot parse JSON", e);
+ } catch (final UnsupportedOperationException e) {
+ Log.e(LOGTAG, "Cannot convert message to JSON", e);
+ }
+ }
+
+ private boolean dispatchToThread(final String type,
+ final NativeJSObject message,
+ final EventCallback callback,
+ final Map<String, List<BundleEventListener>> listenersMap,
+ final Handler thread) {
+ // We need to hold the lock throughout dispatching, to ensure the listeners list
+ // is consistent, while we iterate over it. We don't have to worry about listeners
+ // running for a long time while we have the lock, because the listeners will run
+ // on a separate thread.
+ synchronized (listenersMap) {
+ final List<BundleEventListener> listeners = listenersMap.get(type);
+ if (listeners == null) {
+ return false;
+ }
+
+ if (listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type);
+
+ // There were native listeners, and they're gone.
+ // Dispatch an error rather than looking for more listeners.
+ if (callback != null) {
+ callback.sendError("No listeners for request");
+ }
+ return true;
+ }
+
+ final Bundle messageAsBundle;
+ try {
+ messageAsBundle = message.toBundle();
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ Log.e(LOGTAG, "Exception occurred while handling " + type, e);
+ return true;
+ }
+
+ // Event listeners will call | callback.sendError | if applicable.
+ for (final BundleEventListener listener : listeners) {
+ thread.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.handleMessage(type, messageAsBundle, callback);
+ }
+ });
+ }
+ return true;
+ }
+ }
+
+ public void dispatchEvent(final JSONObject message, final EventCallback callback) {
+ // {
+ // "type": "value",
+ // "event_specific": "value",
+ // ...
+ try {
+ final String type = message.getString("type");
+
+ List<GeckoEventListener> listeners;
+ synchronized (mGeckoThreadJSONListeners) {
+ listeners = mGeckoThreadJSONListeners.get(type);
+ }
+ if (listeners == null || listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type);
+
+ // If there are no listeners, dispatch an error.
+ if (callback != null) {
+ callback.sendError("No listeners for request");
+ }
+ return;
+ }
+ for (final GeckoEventListener listener : listeners) {
+ listener.handleMessage(type, message);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
+ }
+ }
+
+ @RobocopTarget
+ @Deprecated
+ public static void sendResponse(JSONObject message, Object response) {
+ sendResponseHelper(STATUS_SUCCESS, message, response);
+ }
+
+ @Deprecated
+ public static void sendError(JSONObject message, Object response) {
+ sendResponseHelper(STATUS_ERROR, message, response);
+ }
+
+ @Deprecated
+ private static void sendResponseHelper(String status, JSONObject message, Object response) {
+ try {
+ final String topic = message.getString("type") + ":Response";
+ final JSONObject wrapper = new JSONObject();
+ wrapper.put(GUID, message.getString(GUID));
+ wrapper.put("status", status);
+ wrapper.put("response", response);
+
+ if (ThreadUtils.isOnGeckoThread()) {
+ GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
+ } else {
+ GeckoAppShell.notifyObservers(topic, wrapper.toString(),
+ GeckoThread.State.PROFILE_READY);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Unable to send response", e);
+ }
+ }
+
+ private static class GeckoEventCallback implements EventCallback {
+ private final String guid;
+ private final String type;
+ private boolean sent;
+
+ public GeckoEventCallback(final String guid, final String type) {
+ this.guid = guid;
+ this.type = type;
+ }
+
+ @Override
+ public void sendSuccess(final Object response) {
+ sendResponse(STATUS_SUCCESS, response);
+ }
+
+ @Override
+ public void sendError(final Object response) {
+ sendResponse(STATUS_ERROR, response);
+ }
+
+ private void sendResponse(final String status, final Object response) {
+ if (sent) {
+ throw new IllegalStateException("Callback has already been executed for type=" +
+ type + ", guid=" + guid);
+ }
+
+ sent = true;
+
+ try {
+ final String topic = type + ":Response";
+ final JSONObject wrapper = new JSONObject();
+ wrapper.put(GUID, guid);
+ wrapper.put("status", status);
+ wrapper.put("response", response);
+
+ if (ThreadUtils.isOnGeckoThread()) {
+ GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
+ } else {
+ GeckoAppShell.notifyObservers(topic, wrapper.toString(),
+ GeckoThread.State.PROFILE_READY);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Unable to send response for: " + type, e);
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -0,0 +1,413 @@
+/* -*- 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.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UIAsyncTask;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+
+import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
+import com.googlecode.eyesfree.braille.selfbraille.WriteData;
+
+public class GeckoAccessibility {
+ private static final String LOGTAG = "GeckoAccessibility";
+ private static final int VIRTUAL_ENTRY_POINT_BEFORE = 1;
+ private static final int VIRTUAL_CURSOR_POSITION = 2;
+ private static final int VIRTUAL_ENTRY_POINT_AFTER = 3;
+
+ private static boolean sEnabled;
+ // Used to store the JSON message and populate the event later in the code path.
+ private static JSONObject sHoverEnter;
+ private static AccessibilityNodeInfo sVirtualCursorNode;
+ private static int sCurrentNode;
+
+ // This is the number Brailleback uses to start indexing routing keys.
+ private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
+ private static SelfBrailleClient sSelfBrailleClient;
+
+ public static void updateAccessibilitySettings (final Context context) {
+ new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
+ @Override
+ public Void doInBackground() {
+ JSONObject ret = new JSONObject();
+ sEnabled = false;
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ sEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();
+ if (Versions.feature16Plus && sEnabled && sSelfBrailleClient == null) {
+ sSelfBrailleClient = new SelfBrailleClient(GeckoAppShell.getContext(), false);
+ }
+
+ try {
+ ret.put("enabled", sEnabled);
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "Error building JSON arguments for Accessibility:Settings:", ex);
+ }
+
+ GeckoAppShell.notifyObservers("Accessibility:Settings", ret.toString());
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Void args) {
+ final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.setAccessibilityEnabled(sEnabled);
+ }
+ }.execute();
+ }
+
+ private static void populateEventFromJSON (AccessibilityEvent event, JSONObject message) {
+ final JSONArray textArray = message.optJSONArray("text");
+ if (textArray != null) {
+ for (int i = 0; i < textArray.length(); i++)
+ event.getText().add(textArray.optString(i));
+ }
+
+ event.setContentDescription(message.optString("description"));
+ event.setEnabled(message.optBoolean("enabled", true));
+ event.setChecked(message.optBoolean("checked"));
+ event.setPassword(message.optBoolean("password"));
+ event.setAddedCount(message.optInt("addedCount", -1));
+ event.setRemovedCount(message.optInt("removedCount", -1));
+ event.setFromIndex(message.optInt("fromIndex", -1));
+ event.setItemCount(message.optInt("itemCount", -1));
+ event.setCurrentItemIndex(message.optInt("currentItemIndex", -1));
+ event.setBeforeText(message.optString("beforeText"));
+ if (Versions.feature14Plus) {
+ event.setToIndex(message.optInt("toIndex", -1));
+ event.setScrollable(message.optBoolean("scrollable"));
+ event.setScrollX(message.optInt("scrollX", -1));
+ event.setScrollY(message.optInt("scrollY", -1));
+ }
+ if (Versions.feature15Plus) {
+ event.setMaxScrollX(message.optInt("maxScrollX", -1));
+ event.setMaxScrollY(message.optInt("maxScrollY", -1));
+ }
+ }
+
+ private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
+ final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
+ accEvent.setClassName(GeckoAccessibility.class.getName());
+ accEvent.setPackageName(GeckoAppShell.getContext().getPackageName());
+ populateEventFromJSON(accEvent, message);
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) GeckoAppShell.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+ try {
+ accessibilityManager.sendAccessibilityEvent(accEvent);
+ } catch (IllegalStateException e) {
+ // Accessibility is off.
+ }
+ }
+
+ public static boolean isEnabled() {
+ return sEnabled;
+ }
+
+ public static void sendAccessibilityEvent (final JSONObject message) {
+ if (!sEnabled)
+ return;
+
+ final int eventType = message.optInt("eventType", -1);
+ if (eventType < 0) {
+ Log.e(LOGTAG, "No accessibility event type provided");
+ return;
+ }
+
+ sendAccessibilityEvent(message, eventType);
+ }
+
+ public static void sendAccessibilityEvent (final JSONObject message, final int eventType) {
+ if (!sEnabled)
+ return;
+
+ final String exitView = message.optString("exitView");
+ if (exitView.equals("moveNext")) {
+ sCurrentNode = VIRTUAL_ENTRY_POINT_AFTER;
+ } else if (exitView.equals("movePrevious")) {
+ sCurrentNode = VIRTUAL_ENTRY_POINT_BEFORE;
+ } else {
+ sCurrentNode = VIRTUAL_CURSOR_POSITION;
+ }
+
+ if (Versions.preJB) {
+ // Before Jelly Bean we send events directly from here while spoofing the source by setting
+ // the package and class name manually.
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ sendDirectAccessibilityEvent(eventType, message);
+ }
+ });
+ } else {
+ // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
+ // it work with TalkBack.
+ final LayerView view = GeckoAppShell.getLayerView();
+ if (view == null)
+ return;
+
+ if (sVirtualCursorNode == null)
+ sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
+ sVirtualCursorNode.setEnabled(message.optBoolean("enabled", true));
+ sVirtualCursorNode.setClickable(message.optBoolean("clickable"));
+ sVirtualCursorNode.setCheckable(message.optBoolean("checkable"));
+ sVirtualCursorNode.setChecked(message.optBoolean("checked"));
+ sVirtualCursorNode.setPassword(message.optBoolean("password"));
+
+ final JSONArray textArray = message.optJSONArray("text");
+ StringBuilder sb = new StringBuilder();
+ if (textArray != null && textArray.length() > 0) {
+ sb.append(textArray.optString(0));
+ for (int i = 1; i < textArray.length(); i++) {
+ sb.append(" ").append(textArray.optString(i));
+ }
+ sVirtualCursorNode.setText(sb.toString());
+ }
+ sVirtualCursorNode.setContentDescription(message.optString("description"));
+
+ JSONObject bounds = message.optJSONObject("bounds");
+ if (bounds != null) {
+ Rect relativeBounds = new Rect(bounds.optInt("left"), bounds.optInt("top"),
+ bounds.optInt("right"), bounds.optInt("bottom"));
+ sVirtualCursorNode.setBoundsInParent(relativeBounds);
+ int[] locationOnScreen = new int[2];
+ view.getLocationOnScreen(locationOnScreen);
+ Rect screenBounds = new Rect(relativeBounds);
+ screenBounds.offset(locationOnScreen[0], locationOnScreen[1]);
+ sVirtualCursorNode.setBoundsInScreen(screenBounds);
+ }
+
+ final JSONObject braille = message.optJSONObject("brailleOutput");
+ if (braille != null) {
+ sendBrailleText(view, braille.optString("text"),
+ braille.optInt("selectionStart"), braille.optInt("selectionEnd"));
+ }
+
+ if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
+ sHoverEnter = message;
+ }
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setPackageName(GeckoAppShell.getContext().getPackageName());
+ event.setClassName(GeckoAccessibility.class.getName());
+ if (eventType == AccessibilityEvent.TYPE_ANNOUNCEMENT ||
+ eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ event.setSource(view, View.NO_ID);
+ } else {
+ event.setSource(view, VIRTUAL_CURSOR_POSITION);
+ }
+ populateEventFromJSON(event, message);
+ view.requestSendAccessibilityEvent(view, event);
+ }
+ });
+
+ }
+ }
+
+ private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
+ WriteData data = WriteData.forInfo(info);
+ data.setText(text);
+ // Set either the focus blink or the current caret position/selection
+ data.setSelectionStart(selectionStart);
+ data.setSelectionEnd(selectionEnd);
+ sSelfBrailleClient.write(data);
+ }
+
+ public static void setDelegate(LayerView layerview) {
+ // Only use this delegate in Jelly Bean.
+ if (Versions.feature16Plus) {
+ layerview.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
+ layerview.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ public static void setAccessibilityManagerListeners(final Context context) {
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+ accessibilityManager.addAccessibilityStateChangeListener(new AccessibilityManager.AccessibilityStateChangeListener() {
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ updateAccessibilitySettings(context);
+ }
+ });
+
+ if (Versions.feature19Plus) {
+ accessibilityManager.addTouchExplorationStateChangeListener(new AccessibilityManager.TouchExplorationStateChangeListener() {
+ @Override
+ public void onTouchExplorationStateChanged(boolean enabled) {
+ updateAccessibilitySettings(context);
+ }
+ });
+ }
+ }
+
+ public static void onLayerViewFocusChanged(LayerView layerview, boolean gainFocus) {
+ if (sEnabled)
+ GeckoAppShell.notifyObservers("Accessibility:Focus", gainFocus ? "true" : "false");
+ }
+
+ public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
+ AccessibilityNodeProvider mAccessibilityNodeProvider;
+
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
+ if (mAccessibilityNodeProvider == null)
+ // The accessibility node structure for web content consists of 3 LayerView child nodes:
+ // 1. VIRTUAL_ENTRY_POINT_BEFORE: Represents the entry point before the LayerView.
+ // 2. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
+ // 3. VIRTUAL_ENTRY_POINT_AFTER: Represents the entry point after the LayerView.
+ mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
+ AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CURSOR_POSITION && sVirtualCursorNode != null) ?
+ AccessibilityNodeInfo.obtain(sVirtualCursorNode) :
+ AccessibilityNodeInfo.obtain(host, virtualDescendantId);
+
+ switch (virtualDescendantId) {
+ case View.NO_ID:
+ // This is the parent LayerView node, populate it with children.
+ onInitializeAccessibilityNodeInfo(host, info);
+ info.addChild(host, VIRTUAL_ENTRY_POINT_BEFORE);
+ info.addChild(host, VIRTUAL_CURSOR_POSITION);
+ info.addChild(host, VIRTUAL_ENTRY_POINT_AFTER);
+ break;
+ default:
+ info.setParent(host);
+ info.setSource(host, virtualDescendantId);
+ info.setVisibleToUser(host.isShown());
+ info.setPackageName(GeckoAppShell.getContext().getPackageName());
+ info.setClassName(host.getClass().getName());
+ info.setEnabled(true);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+ info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+ info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
+ info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
+ info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
+ info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
+ break;
+ }
+ return info;
+ }
+
+ @Override
+ public boolean performAction (int virtualViewId, int action, Bundle arguments) {
+ if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
+ // The accessibility focus is permanently on the middle node, VIRTUAL_CURSOR_POSITION.
+ // When we enter the view forward or backward we just ask Gecko to get focus, keeping the current position.
+ if (virtualViewId == VIRTUAL_CURSOR_POSITION && sHoverEnter != null) {
+ GeckoAccessibility.sendAccessibilityEvent(sHoverEnter, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ } else {
+ GeckoAppShell.notifyObservers("Accessibility:Focus", "true");
+ }
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ActivateObject", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:LongPress", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ScrollForward", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ScrollBackward", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ String traversalRule = "";
+ if (arguments != null) {
+ traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+ }
+ GeckoAppShell.notifyObservers("Accessibility:NextObject", traversalRule);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ String traversalRule = "";
+ if (arguments != null) {
+ traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+ }
+ GeckoAppShell.notifyObservers("Accessibility:PreviousObject", traversalRule);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY &&
+ virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ // XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
+ // the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
+ // Other negative values are used by ChromeVox, but we don't support them.
+ // FAKE_GRANULARITY_READ_CURRENT = -1
+ // FAKE_GRANULARITY_READ_TITLE = -2
+ // FAKE_GRANULARITY_STOP_SPEECH = -3
+ // FAKE_GRANULARITY_CHANGE_SHIFTER = -4
+ int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
+ int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
+ JSONObject activationData = new JSONObject();
+ try {
+ activationData.put("keyIndex", keyIndex);
+ } catch (JSONException e) {
+ return true;
+ }
+ GeckoAppShell.notifyObservers("Accessibility:ActivateObject", activationData.toString());
+ } else if (granularity > 0) {
+ JSONObject movementData = new JSONObject();
+ try {
+ movementData.put("direction", "Next");
+ movementData.put("granularity", granularity);
+ } catch (JSONException e) {
+ return true;
+ }
+ GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
+ }
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY &&
+ virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ JSONObject movementData = new JSONObject();
+ int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ try {
+ movementData.put("direction", "Previous");
+ movementData.put("granularity", granularity);
+ } catch (JSONException e) {
+ return true;
+ }
+ if (granularity > 0) {
+ GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
+ }
+ return true;
+ }
+ return host.performAccessibilityAction(action, arguments);
+ }
+ };
+
+ return mAccessibilityNodeProvider;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
@@ -0,0 +1,2362 @@
+/* -*- 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.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URLConnection;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.annotation.SuppressLint;
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.PanZoomController;
+import org.mozilla.gecko.permissions.Permissions;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoRequest;
+import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSContainer;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ProxySelector;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.Signature;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.hardware.Sensor;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Looper;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.HapticFeedbackConstants;
+import android.view.Surface;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.WindowManager;
+import android.webkit.MimeTypeMap;
+import android.widget.AbsoluteLayout;
+
+public class GeckoAppShell
+{
+ private static final String LOGTAG = "GeckoAppShell";
+
+ // We have static members only.
+ private GeckoAppShell() { }
+
+ public static final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
+ public static final String ACTION_HOMESCREEN_SHORTCUT = "org.mozilla.gecko.BOOKMARK";
+ public static final String PREFS_OOM_EXCEPTION = "OOMException";
+
+ private static final CrashHandler CRASH_HANDLER = new CrashHandler() {
+ @Override
+ protected String getAppPackageName() {
+ return AppConstants.ANDROID_PACKAGE_NAME;
+ }
+
+ @Override
+ protected Context getAppContext() {
+ return sContextGetter != null ? getApplicationContext() : null;
+ }
+
+ @Override
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Bundle extras = super.getCrashExtras(thread, exc);
+
+ extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
+ extras.putString("ProductID", AppConstants.MOZ_APP_ID);
+ extras.putString("Version", AppConstants.MOZ_APP_VERSION);
+ extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
+ extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
+ extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
+ return extras;
+ }
+
+ @Override
+ public void uncaughtException(final Thread thread, final Throwable exc) {
+ if (GeckoThread.isState(GeckoThread.State.EXITING) ||
+ GeckoThread.isState(GeckoThread.State.EXITED)) {
+ // We've called System.exit. All exceptions after this point are Android
+ // berating us for being nasty to it.
+ return;
+ }
+
+ super.uncaughtException(thread, exc);
+ }
+
+ @Override
+ public boolean reportException(final Thread thread, final Throwable exc) {
+ try {
+ if (exc instanceof OutOfMemoryError) {
+ SharedPreferences prefs = getSharedPreferences();
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREFS_OOM_EXCEPTION, true);
+
+ // Synchronously write to disk so we know it's done before we
+ // shutdown
+ editor.commit();
+ }
+
+ reportJavaCrash(getExceptionStackTrace(exc));
+
+ } catch (final Throwable e) {
+ }
+
+ // reportJavaCrash should have caused us to hard crash. If we're still here,
+ // it probably means Gecko is not loaded, and we should do something else.
+ if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
+ // Only use Java crash reporter if enabled on official build.
+ return super.reportException(thread, exc);
+ }
+ return false;
+ }
+ };
+
+ public static CrashHandler ensureCrashHandling() {
+ // Crash handling is automatically enabled when GeckoAppShell is loaded.
+ return CRASH_HANDLER;
+ }
+
+ private static final Map<String, String> ALERT_COOKIES = new ConcurrentHashMap<String, String>();
+
+ private static volatile boolean locationHighAccuracyEnabled;
+
+ // Accessed by NotificationHelper. This should be encapsulated.
+ /* package */ static NotificationClient notificationClient;
+
+ // See also HardwareUtils.LOW_MEMORY_THRESHOLD_MB.
+ private static final int HIGH_MEMORY_DEVICE_THRESHOLD_MB = 768;
+
+ static private int sDensityDpi;
+ static private int sScreenDepth;
+
+ /* Is the value in sVibrationEndTime valid? */
+ private static boolean sVibrationMaybePlaying;
+
+ /* Time (in System.nanoTime() units) when the currently-playing vibration
+ * is scheduled to end. This value is valid only when
+ * sVibrationMaybePlaying is true. */
+ private static long sVibrationEndTime;
+
+ private static Sensor gAccelerometerSensor;
+ private static Sensor gLinearAccelerometerSensor;
+ private static Sensor gGyroscopeSensor;
+ private static Sensor gOrientationSensor;
+ private static Sensor gProximitySensor;
+ private static Sensor gLightSensor;
+ private static Sensor gRotationVectorSensor;
+ private static Sensor gGameRotationVectorSensor;
+
+ private static final String GECKOREQUEST_RESPONSE_KEY = "response";
+ private static final String GECKOREQUEST_ERROR_KEY = "error";
+
+ /*
+ * Keep in sync with constants found here:
+ * http://mxr.mozilla.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl
+ */
+ static public final int WPL_STATE_START = 0x00000001;
+ static public final int WPL_STATE_STOP = 0x00000010;
+ static public final int WPL_STATE_IS_DOCUMENT = 0x00020000;
+ static public final int WPL_STATE_IS_NETWORK = 0x00040000;
+
+ /* Keep in sync with constants found here:
+ http://mxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ static public final int LINK_TYPE_UNKNOWN = 0;
+ static public final int LINK_TYPE_ETHERNET = 1;
+ static public final int LINK_TYPE_USB = 2;
+ static public final int LINK_TYPE_WIFI = 3;
+ static public final int LINK_TYPE_WIMAX = 4;
+ static public final int LINK_TYPE_2G = 5;
+ static public final int LINK_TYPE_3G = 6;
+ static public final int LINK_TYPE_4G = 7;
+
+ /* The Android-side API: API methods that Android calls */
+
+ // Initialization methods
+ public static native void registerJavaUiThread();
+
+ // helper methods
+ public static void callObserver(String observerKey, String topic, String data) {
+ sendEventToGecko(GeckoEvent.createCallObserverEvent(observerKey, topic, data));
+ }
+ public static void removeObserver(String observerKey) {
+ sendEventToGecko(GeckoEvent.createRemoveObserverEvent(observerKey));
+ }
+ public static native void onSurfaceTextureFrameAvailable(Object surfaceTexture, int id);
+ public static native void dispatchMemoryPressure();
+
+ private static native void reportJavaCrash(String stackTrace);
+
+ public static void notifyUriVisited(String uri) {
+ sendEventToGecko(GeckoEvent.createVisitedEvent(uri));
+ }
+
+ public static native void notifyBatteryChange(double aLevel, boolean aCharging, double aRemainingTime);
+
+ public static native void invalidateAndScheduleComposite();
+
+ public static native float computeRenderIntegrity();
+
+ public static native SurfaceBits getSurfaceBits(Surface surface);
+
+ public static native void addPresentationSurface(Surface surface);
+ public static native void removePresentationSurface(Surface surface);
+
+ public static native void onFullScreenPluginHidden(View view);
+
+ private static LayerView sLayerView;
+ private static Rect sScreenSize;
+
+ public static void setLayerView(LayerView lv) {
+ if (sLayerView == lv) {
+ return;
+ }
+ sLayerView = lv;
+ }
+
+ @RobocopTarget
+ public static LayerView getLayerView() {
+ return sLayerView;
+ }
+
+ /**
+ * If the Gecko thread is running, immediately dispatches the event to
+ * Gecko.
+ *
+ * If the Gecko thread is not running, queues the event. If the queue is
+ * full, throws {@link IllegalStateException}.
+ *
+ * Queued events will be dispatched in order of arrival when the Gecko
+ * thread becomes live.
+ *
+ * This method can be called from any thread.
+ *
+ * @param e
+ * the event to dispatch. Cannot be null.
+ */
+ @RobocopTarget
+ public static void sendEventToGecko(GeckoEvent e) {
+ if (e == null) {
+ throw new IllegalArgumentException("e cannot be null.");
+ }
+
+ if (GeckoThread.isRunning()) {
+ notifyGeckoOfEvent(e);
+ // Gecko will copy the event data into a normal C++ object.
+ // We can recycle the event now.
+ e.recycle();
+ return;
+ }
+
+ GeckoThread.addPendingEvent(e);
+ }
+
+ /**
+ * Sends an asynchronous request to Gecko.
+ *
+ * The response data will be passed to {@link GeckoRequest#onResponse(NativeJSObject)} if the
+ * request succeeds; otherwise, {@link GeckoRequest#onError()} will fire.
+ *
+ * This method follows the same queuing conditions as {@link #sendEventToGecko(GeckoEvent)}.
+ * It can be called from any thread. The GeckoRequest callbacks will be executed on the Gecko thread.
+ *
+ * @param request The request to dispatch. Cannot be null.
+ */
+ @RobocopTarget
+ public static void sendRequestToGecko(final GeckoRequest request) {
+ final String responseMessage = "Gecko:Request" + request.getId();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() {
+ @Override
+ public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this, event);
+ if (!message.has(GECKOREQUEST_RESPONSE_KEY)) {
+ request.onError(message.getObject(GECKOREQUEST_ERROR_KEY));
+ return;
+ }
+ request.onResponse(message.getObject(GECKOREQUEST_RESPONSE_KEY));
+ }
+ }, responseMessage);
+
+ notifyObservers(request.getName(), request.getData());
+ }
+
+ // Tell the Gecko event loop that an event is available.
+ public static native void notifyGeckoOfEvent(GeckoEvent event);
+
+ // Synchronously notify a Gecko observer; must be called from Gecko thread.
+ @WrapForJNI
+ public static native void syncNotifyObservers(String topic, String data);
+
+ @WrapForJNI(stubName = "NotifyObservers")
+ private static native void nativeNotifyObservers(String topic, String data);
+
+ @RobocopTarget
+ public static void notifyObservers(final String topic, final String data) {
+ notifyObservers(topic, data, GeckoThread.State.RUNNING);
+ }
+
+ public static void notifyObservers(final String topic, final String data, final GeckoThread.State state) {
+ if (GeckoThread.isStateAtLeast(state)) {
+ nativeNotifyObservers(topic, data);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ state, GeckoAppShell.class, "nativeNotifyObservers",
+ String.class, topic, String.class, data);
+ }
+ }
+
+ /*
+ * The Gecko-side API: API methods that Gecko calls
+ */
+
+ @WrapForJNI(allowMultithread = true, noThrow = true)
+ public static String handleUncaughtException(Throwable e) {
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ final Throwable exc = CrashHandler.getRootException(e);
+ final StackTraceElement[] stack = exc.getStackTrace();
+ if (stack.length >= 1 && stack[0].isNativeMethod()) {
+ // The exception occurred when running native code. Return an exception
+ // string and trigger the crash reporter inside the caller so that we get
+ // a better native stack in Socorro.
+ CrashHandler.logException(Thread.currentThread(), exc);
+ return CrashHandler.getExceptionStackTrace(exc);
+ }
+ }
+ CRASH_HANDLER.uncaughtException(null, e);
+ return null;
+ }
+
+ private static final Runnable sCallbackRunnable = new Runnable() {
+ @Override
+ public void run() {
+ ThreadUtils.assertOnUiThread();
+ long nextDelay = runUiThreadCallback();
+ if (nextDelay >= 0) {
+ ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
+ }
+ }
+ };
+
+ private static native long runUiThreadCallback();
+
+ @WrapForJNI(allowMultithread = true)
+ private static void requestUiThreadCallback(long delay) {
+ ThreadUtils.getUiHandler().postDelayed(sCallbackRunnable, delay);
+ }
+
+ private static float getLocationAccuracy(Location location) {
+ float radius = location.getAccuracy();
+ return (location.hasAccuracy() && radius > 0) ? radius : 1001;
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in enableLocation()
+ private static Location getLastKnownLocation(LocationManager lm) {
+ Location lastKnownLocation = null;
+ List<String> providers = lm.getAllProviders();
+
+ for (String provider : providers) {
+ Location location = lm.getLastKnownLocation(provider);
+ if (location == null) {
+ continue;
+ }
+
+ if (lastKnownLocation == null) {
+ lastKnownLocation = location;
+ continue;
+ }
+
+ long timeDiff = location.getTime() - lastKnownLocation.getTime();
+ if (timeDiff > 0 ||
+ (timeDiff == 0 &&
+ getLocationAccuracy(location) < getLocationAccuracy(lastKnownLocation))) {
+ lastKnownLocation = location;
+ }
+ }
+
+ return lastKnownLocation;
+ }
+
+ @WrapForJNI
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for within this method
+ public static void enableLocation(final boolean enable) {
+ Permissions
+ .from((Activity) getContext())
+ .withPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
+ .onUIThread()
+ .doNotPromptIf(!enable)
+ .run(new Runnable() {
+ @Override
+ public void run() {
+ LocationManager lm = getLocationManager(getApplicationContext());
+ if (lm == null) {
+ return;
+ }
+
+ if (enable) {
+ Location lastKnownLocation = getLastKnownLocation(lm);
+ if (lastKnownLocation != null) {
+ getGeckoInterface().getLocationListener().onLocationChanged(lastKnownLocation);
+ }
+
+ Criteria criteria = new Criteria();
+ criteria.setSpeedRequired(false);
+ criteria.setBearingRequired(false);
+ criteria.setAltitudeRequired(false);
+ if (locationHighAccuracyEnabled) {
+ criteria.setAccuracy(Criteria.ACCURACY_FINE);
+ criteria.setCostAllowed(true);
+ criteria.setPowerRequirement(Criteria.POWER_HIGH);
+ } else {
+ criteria.setAccuracy(Criteria.ACCURACY_COARSE);
+ criteria.setCostAllowed(false);
+ criteria.setPowerRequirement(Criteria.POWER_LOW);
+ }
+
+ String provider = lm.getBestProvider(criteria, true);
+ if (provider == null)
+ return;
+
+ Looper l = Looper.getMainLooper();
+ lm.requestLocationUpdates(provider, 100, (float) .5, getGeckoInterface().getLocationListener(), l);
+ } else {
+ lm.removeUpdates(getGeckoInterface().getLocationListener());
+ }
+ }
+ });
+ }
+
+ private static LocationManager getLocationManager(Context context) {
+ try {
+ return (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ } catch (NoSuchFieldError e) {
+ // Some Tegras throw exceptions about missing the CONTROL_LOCATION_UPDATES permission,
+ // which allows enabling/disabling location update notifications from the cell radio.
+ // CONTROL_LOCATION_UPDATES is not for use by normal applications, but we might be
+ // hitting this problem if the Tegras are confused about missing cell radios.
+ Log.e(LOGTAG, "LOCATION_SERVICE not found?!", e);
+ return null;
+ }
+ }
+
+ @WrapForJNI
+ public static void enableLocationHighAccuracy(final boolean enable) {
+ locationHighAccuracyEnabled = enable;
+ }
+
+ @WrapForJNI
+ public static boolean setAlarm(int aSeconds, int aNanoSeconds) {
+ AlarmManager am = (AlarmManager)
+ getApplicationContext().getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
+ PendingIntent pi = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // AlarmManager only supports millisecond precision
+ long time = ((long) aSeconds * 1000) + ((long) aNanoSeconds / 1_000_000L);
+ am.setExact(AlarmManager.RTC_WAKEUP, time, pi);
+
+ return true;
+ }
+
+ @WrapForJNI
+ public static void disableAlarm() {
+ AlarmManager am = (AlarmManager)
+ getApplicationContext().getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
+ PendingIntent pi = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ am.cancel(pi);
+ }
+
+ @WrapForJNI
+ public static void enableSensor(int aSensortype) {
+ GeckoInterface gi = getGeckoInterface();
+ if (gi == null) {
+ return;
+ }
+ SensorManager sm = (SensorManager)
+ getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
+
+ switch (aSensortype) {
+ case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
+ if (gGameRotationVectorSensor == null) {
+ gGameRotationVectorSensor = sm.getDefaultSensor(15);
+ // sm.getDefaultSensor(
+ // Sensor.TYPE_GAME_ROTATION_VECTOR); // API >= 18
+ }
+ if (gGameRotationVectorSensor != null) {
+ sm.registerListener(gi.getSensorEventListener(),
+ gGameRotationVectorSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ if (gGameRotationVectorSensor != null) {
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
+ if (gRotationVectorSensor == null) {
+ gRotationVectorSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ROTATION_VECTOR);
+ }
+ if (gRotationVectorSensor != null) {
+ sm.registerListener(gi.getSensorEventListener(),
+ gRotationVectorSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ if (gRotationVectorSensor != null) {
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ORIENTATION:
+ if (gOrientationSensor == null) {
+ gOrientationSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ORIENTATION);
+ }
+ if (gOrientationSensor != null) {
+ sm.registerListener(gi.getSensorEventListener(),
+ gOrientationSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_ACCELERATION:
+ if (gAccelerometerSensor == null) {
+ gAccelerometerSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ACCELEROMETER);
+ }
+ if (gAccelerometerSensor != null) {
+ sm.registerListener(gi.getSensorEventListener(),
+ gAccelerometerSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_PROXIMITY:
+ if (gProximitySensor == null) {
+ gProximitySensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ }
+ if (gProximitySensor != null) {
+ sm.registerListener(gi.getSensorEventListener(),
+ gProximitySensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LIGHT:
+ if (gLightSensor == null) {
+ gLightSensor = sm.getDefaultSensor(Sensor.TYPE_LIGHT);
+ }
+ if (gLightSensor != null) {
+ sm.registerListener(gi.getSensorEventListener(),
+ gLightSensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
+ if (gLinearAccelerometerSensor == null) {
+ gLinearAccelerometerSensor = sm.getDefaultSensor(
+ Sensor.TYPE_LINEAR_ACCELERATION);
+ }
+ if (gLinearAccelerometerSensor != null) {
+ sm.registerListener(gi.getSensorEventListener(),
+ gLinearAccelerometerSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_GYROSCOPE:
+ if (gGyroscopeSensor == null) {
+ gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ }
+ if (gGyroscopeSensor != null) {
+ sm.registerListener(gi.getSensorEventListener(),
+ gGyroscopeSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ default:
+ Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " +
+ aSensortype);
+ }
+ }
+
+ @WrapForJNI
+ public static void disableSensor(int aSensortype) {
+ GeckoInterface gi = getGeckoInterface();
+ if (gi == null)
+ return;
+
+ SensorManager sm = (SensorManager)
+ getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
+
+ switch (aSensortype) {
+ case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
+ if (gGameRotationVectorSensor != null) {
+ sm.unregisterListener(gi.getSensorEventListener(), gGameRotationVectorSensor);
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
+ if (gRotationVectorSensor != null) {
+ sm.unregisterListener(gi.getSensorEventListener(), gRotationVectorSensor);
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ORIENTATION:
+ if (gOrientationSensor != null) {
+ sm.unregisterListener(gi.getSensorEventListener(), gOrientationSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_ACCELERATION:
+ if (gAccelerometerSensor != null) {
+ sm.unregisterListener(gi.getSensorEventListener(), gAccelerometerSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_PROXIMITY:
+ if (gProximitySensor != null) {
+ sm.unregisterListener(gi.getSensorEventListener(), gProximitySensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LIGHT:
+ if (gLightSensor != null) {
+ sm.unregisterListener(gi.getSensorEventListener(), gLightSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
+ if (gLinearAccelerometerSensor != null) {
+ sm.unregisterListener(gi.getSensorEventListener(), gLinearAccelerometerSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_GYROSCOPE:
+ if (gGyroscopeSensor != null) {
+ sm.unregisterListener(gi.getSensorEventListener(), gGyroscopeSensor);
+ }
+ break;
+ default:
+ Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype);
+ }
+ }
+
+ @WrapForJNI
+ public static void startMonitoringGamepad() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ AndroidGamepadManager.startup();
+ }
+ });
+ }
+
+ @WrapForJNI
+ public static void stopMonitoringGamepad() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ AndroidGamepadManager.shutdown();
+ }
+ });
+ }
+
+ @WrapForJNI
+ public static void gamepadAdded(final int device_id, final int service_id) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ AndroidGamepadManager.gamepadAdded(device_id, service_id);
+ }
+ });
+ }
+
+ @WrapForJNI
+ public static void moveTaskToBack() {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().getActivity().moveTaskToBack(true);
+ }
+
+ @WrapForJNI
+ static void scheduleRestart() {
+ getGeckoInterface().doRestart();
+ }
+
+ // Creates a homescreen shortcut for a web page.
+ // This is the entry point from nsIShellService.
+ @WrapForJNI
+ public static void createShortcut(final String aTitle, final String aURI) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.createShortcut(aTitle, aURI);
+ }
+
+ @JNITarget
+ static public int getPreferredIconSize() {
+ if (Versions.feature11Plus) {
+ ActivityManager am = (ActivityManager)
+ getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
+ return am.getLauncherLargeIconSize();
+ } else {
+ switch (getDpi()) {
+ case DisplayMetrics.DENSITY_MEDIUM:
+ return 48;
+ case DisplayMetrics.DENSITY_XHIGH:
+ return 96;
+ case DisplayMetrics.DENSITY_HIGH:
+ default:
+ return 72;
+ }
+ }
+ }
+
+ @WrapForJNI(stubName = "GetHandlersForMimeTypeWrapper")
+ static String[] getHandlersForMimeType(String aMimeType, String aAction) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return new String[] {};
+ }
+ return geckoInterface.getHandlersForMimeType(aMimeType, aAction);
+ }
+
+ @WrapForJNI(stubName = "GetHandlersForURLWrapper")
+ static String[] getHandlersForURL(String aURL, String aAction) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return new String[] {};
+ }
+ return geckoInterface.getHandlersForURL(aURL, aAction);
+ }
+
+ @WrapForJNI(stubName = "GetHWEncoderCapability")
+ static boolean getHWEncoderCapability() {
+ return HardwareCodecCapabilityUtils.getHWEncoderCapability();
+ }
+
+ @WrapForJNI(stubName = "GetHWDecoderCapability")
+ static boolean getHWDecoderCapability() {
+ return HardwareCodecCapabilityUtils.getHWDecoderCapability();
+ }
+
+ static List<ResolveInfo> queryIntentActivities(Intent intent) {
+ final PackageManager pm = getApplicationContext().getPackageManager();
+
+ // Exclude any non-exported activities: we can't open them even if we want to!
+ // Bug 1031569 has some details.
+ final ArrayList<ResolveInfo> list = new ArrayList<>();
+ for (ResolveInfo ri: pm.queryIntentActivities(intent, 0)) {
+ if (ri.activityInfo.exported) {
+ list.add(ri);
+ }
+ }
+
+ return list;
+ }
+
+ @WrapForJNI(stubName = "GetExtensionFromMimeTypeWrapper")
+ static String getExtensionFromMimeType(String aMimeType) {
+ return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
+ }
+
+ @WrapForJNI(stubName = "GetMimeTypeFromExtensionsWrapper")
+ static String getMimeTypeFromExtensions(String aFileExt) {
+ StringTokenizer st = new StringTokenizer(aFileExt, ".,; ");
+ String type = null;
+ String subType = null;
+ while (st.hasMoreElements()) {
+ String ext = st.nextToken();
+ String mt = getMimeTypeFromExtension(ext);
+ if (mt == null)
+ continue;
+ int slash = mt.indexOf('/');
+ String tmpType = mt.substring(0, slash);
+ if (!tmpType.equalsIgnoreCase(type))
+ type = type == null ? tmpType : "*";
+ String tmpSubType = mt.substring(slash + 1);
+ if (!tmpSubType.equalsIgnoreCase(subType))
+ subType = subType == null ? tmpSubType : "*";
+ }
+ if (type == null)
+ type = "*";
+ if (subType == null)
+ subType = "*";
+ return type + "/" + subType;
+ }
+
+ static boolean isUriSafeForScheme(Uri aUri) {
+ // Bug 794034 - We don't want to pass MWI or USSD codes to the
+ // dialer, and ensure the Uri class doesn't parse a URI
+ // containing a fragment ('#')
+ final String scheme = aUri.getScheme();
+ if ("tel".equals(scheme) || "sms".equals(scheme)) {
+ final String number = aUri.getSchemeSpecificPart();
+ if (number.contains("#") || number.contains("*") || aUri.getFragment() != null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @WrapForJNI
+ public static boolean openUriExternal(String targetURI,
+ String mimeType,
+ String packageName,
+ String className,
+ String action,
+ String title) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return false;
+ }
+ return geckoInterface.openUriExternal(targetURI, mimeType, packageName, className, action, title);
+ }
+
+ /**
+ * Only called from GeckoApp.
+ */
+ public static void setNotificationClient(NotificationClient client) {
+ if (notificationClient == null) {
+ notificationClient = client;
+ } else {
+ Log.d(LOGTAG, "Notification client already set");
+ }
+ }
+
+ @WrapForJNI(stubName = "ShowPersistentAlertNotificationWrapper")
+ public static void showPersistentAlertNotification(
+ String aPersistentData,
+ String aImageUrl, String aAlertTitle, String aAlertText,
+ String aAlertCookie, String aAlertName, String aHost) {
+ Intent notificationIntent = GeckoService.getIntentToCreateServices(
+ getApplicationContext(), "persistent-notification-click", aPersistentData);
+ int notificationID = aAlertName.hashCode();
+ PendingIntent contentIntent = PendingIntent.getService(
+ getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
+ }
+
+ @WrapForJNI(stubName = "ShowAlertNotificationWrapper")
+ public static void showAlertNotification(String aImageUrl, String aAlertTitle, String aAlertText, String aAlertCookie, String aAlertName, String aHost) {
+ // The intent to launch when the user clicks the expanded notification
+ Intent notificationIntent = new Intent(ACTION_ALERT_CALLBACK);
+ notificationIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+ notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ int notificationID = aAlertName.hashCode();
+
+ // Put the strings into the intent as an URI "alert:?name=<alertName>&app=<appName>&cookie=<cookie>"
+ Uri.Builder b = new Uri.Builder();
+ Uri dataUri = b.scheme("alert").path(Integer.toString(notificationID))
+ .appendQueryParameter("name", aAlertName)
+ .appendQueryParameter("cookie", aAlertCookie)
+ .build();
+ notificationIntent.setData(dataUri);
+ PendingIntent contentIntent = PendingIntent.getActivity(
+ getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ ALERT_COOKIES.put(aAlertName, aAlertCookie);
+ callObserver(aAlertName, "alertshow", aAlertCookie);
+
+ notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
+ }
+
+ @WrapForJNI
+ public static void alertsProgressListener_OnProgress(String aAlertName, long aProgress, long aProgressMax, String aAlertText) {
+ int notificationID = aAlertName.hashCode();
+ notificationClient.update(notificationID, aProgress, aProgressMax, aAlertText);
+ }
+
+ @WrapForJNI
+ public static void closeNotification(String aAlertName) {
+ String alertCookie = ALERT_COOKIES.get(aAlertName);
+ if (alertCookie != null) {
+ callObserver(aAlertName, "alertfinished", alertCookie);
+ ALERT_COOKIES.remove(aAlertName);
+ }
+
+ removeObserver(aAlertName);
+
+ int notificationID = aAlertName.hashCode();
+ notificationClient.remove(notificationID);
+ }
+
+ public static void handleNotification(String aAction, String aAlertName, String aAlertCookie) {
+ int notificationID = aAlertName.hashCode();
+
+ if (ACTION_ALERT_CALLBACK.equals(aAction)) {
+ callObserver(aAlertName, "alertclickcallback", aAlertCookie);
+
+ if (notificationClient.isOngoing(notificationID)) {
+ // When clicked, keep the notification if it displays progress
+ return;
+ }
+ }
+ closeNotification(aAlertName);
+ }
+
+ @WrapForJNI(stubName = "GetDpiWrapper")
+ public static int getDpi() {
+ if (sDensityDpi == 0) {
+ sDensityDpi = getApplicationContext().getResources().getDisplayMetrics().densityDpi;
+ }
+
+ return sDensityDpi;
+ }
+
+ @WrapForJNI
+ public static float getDensity() {
+ return getApplicationContext().getResources().getDisplayMetrics().density;
+ }
+
+ private static boolean isHighMemoryDevice() {
+ return HardwareUtils.getMemSize() > HIGH_MEMORY_DEVICE_THRESHOLD_MB;
+ }
+
+ /**
+ * Returns the colour depth of the default screen. This will either be
+ * 24 or 16.
+ */
+ @WrapForJNI(stubName = "GetScreenDepthWrapper")
+ public static synchronized int getScreenDepth() {
+ if (sScreenDepth == 0) {
+ sScreenDepth = 16;
+ PixelFormat info = new PixelFormat();
+ final WindowManager wm = (WindowManager)
+ getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ PixelFormat.getPixelFormatInfo(wm.getDefaultDisplay().getPixelFormat(), info);
+ if (info.bitsPerPixel >= 24 && isHighMemoryDevice()) {
+ sScreenDepth = 24;
+ }
+ }
+
+ return sScreenDepth;
+ }
+
+ @WrapForJNI
+ public static synchronized void setScreenDepthOverride(int aScreenDepth) {
+ if (sScreenDepth != 0) {
+ Log.e(LOGTAG, "Tried to override screen depth after it's already been set");
+ return;
+ }
+
+ sScreenDepth = aScreenDepth;
+ }
+
+ @WrapForJNI
+ public static void setFullScreen(boolean fullscreen) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().setFullScreen(fullscreen);
+ }
+
+ @WrapForJNI
+ public static void performHapticFeedback(boolean aIsLongPress) {
+ // Don't perform haptic feedback if a vibration is currently playing,
+ // because the haptic feedback will nuke the vibration.
+ if (!sVibrationMaybePlaying || System.nanoTime() >= sVibrationEndTime) {
+ LayerView layerView = getLayerView();
+ layerView.performHapticFeedback(aIsLongPress ?
+ HapticFeedbackConstants.LONG_PRESS :
+ HapticFeedbackConstants.VIRTUAL_KEY);
+ }
+ }
+
+ private static Vibrator vibrator() {
+ return (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE);
+ }
+
+ // Helper method to convert integer array to long array.
+ private static long[] convertIntToLongArray(int[] input) {
+ long[] output = new long[input.length];
+ for (int i = 0; i < input.length; i++) {
+ output[i] = input[i];
+ }
+ return output;
+ }
+
+ // Vibrate only if haptic feedback is enabled.
+ public static void vibrateOnHapticFeedbackEnabled(int[] milliseconds) {
+ if (Settings.System.getInt(getApplicationContext().getContentResolver(),
+ Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) > 0) {
+ vibrate(convertIntToLongArray(milliseconds), -1);
+ }
+ }
+
+ @WrapForJNI(stubName = "Vibrate1")
+ public static void vibrate(long milliseconds) {
+ sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
+ sVibrationMaybePlaying = true;
+ vibrator().vibrate(milliseconds);
+ }
+
+ @WrapForJNI(stubName = "VibrateA")
+ public static void vibrate(long[] pattern, int repeat) {
+ // If pattern.length is even, the last element in the pattern is a
+ // meaningless delay, so don't include it in vibrationDuration.
+ long vibrationDuration = 0;
+ int iterLen = pattern.length - (pattern.length % 2 == 0 ? 1 : 0);
+ for (int i = 0; i < iterLen; i++) {
+ vibrationDuration += pattern[i];
+ }
+
+ sVibrationEndTime = System.nanoTime() + vibrationDuration * 1000000;
+ sVibrationMaybePlaying = true;
+ vibrator().vibrate(pattern, repeat);
+ }
+
+ @WrapForJNI
+ public static void cancelVibrate() {
+ sVibrationMaybePlaying = false;
+ sVibrationEndTime = 0;
+ vibrator().cancel();
+ }
+
+ @WrapForJNI
+ public static void setKeepScreenOn(final boolean on) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // TODO
+ }
+ });
+ }
+
+ @WrapForJNI
+ public static void notifyDefaultPrevented(final boolean defaultPrevented) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ LayerView view = getLayerView();
+ PanZoomController controller = (view == null ? null : view.getPanZoomController());
+ if (controller != null) {
+ controller.notifyDefaultActionPrevented(defaultPrevented);
+ }
+ }
+ });
+ }
+
+ @WrapForJNI
+ public static boolean isNetworkLinkUp() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ try {
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info == null || !info.isConnected())
+ return false;
+ } catch (SecurityException se) {
+ return false;
+ }
+ return true;
+ }
+
+ @WrapForJNI
+ public static boolean isNetworkLinkKnown() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ try {
+ if (cm.getActiveNetworkInfo() == null)
+ return false;
+ } catch (SecurityException se) {
+ return false;
+ }
+ return true;
+ }
+
+ @WrapForJNI
+ public static int networkLinkType() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info == null) {
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ switch (info.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return LINK_TYPE_ETHERNET;
+ case ConnectivityManager.TYPE_WIFI:
+ return LINK_TYPE_WIFI;
+ case ConnectivityManager.TYPE_WIMAX:
+ return LINK_TYPE_WIMAX;
+ case ConnectivityManager.TYPE_MOBILE:
+ break; // We will handle sub-types after the switch.
+ default:
+ Log.w(LOGTAG, "Ignoring the current network type.");
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ TelephonyManager tm = (TelephonyManager)
+ getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm == null) {
+ Log.e(LOGTAG, "Telephony service does not exist");
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ switch (tm.getNetworkType()) {
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ return LINK_TYPE_2G;
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ return LINK_TYPE_2G; // 2.5G
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ return LINK_TYPE_3G;
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ return LINK_TYPE_3G; // 3.5G
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return LINK_TYPE_3G; // 3.75G
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return LINK_TYPE_4G; // 3.9G
+ case TelephonyManager.NETWORK_TYPE_UNKNOWN:
+ default:
+ Log.w(LOGTAG, "Connected to an unknown mobile network!");
+ return LINK_TYPE_UNKNOWN;
+ }
+ }
+
+ @WrapForJNI(stubName = "GetSystemColoursWrapper")
+ public static int[] getSystemColors() {
+ // attrsAppearance[] must correspond to AndroidSystemColors structure in android/AndroidBridge.h
+ final int[] attrsAppearance = {
+ android.R.attr.textColor,
+ android.R.attr.textColorPrimary,
+ android.R.attr.textColorPrimaryInverse,
+ android.R.attr.textColorSecondary,
+ android.R.attr.textColorSecondaryInverse,
+ android.R.attr.textColorTertiary,
+ android.R.attr.textColorTertiaryInverse,
+ android.R.attr.textColorHighlight,
+ android.R.attr.colorForeground,
+ android.R.attr.colorBackground,
+ android.R.attr.panelColorForeground,
+ android.R.attr.panelColorBackground
+ };
+
+ int[] result = new int[attrsAppearance.length];
+
+ final ContextThemeWrapper contextThemeWrapper =
+ new ContextThemeWrapper(getApplicationContext(), android.R.style.TextAppearance);
+
+ final TypedArray appearance = contextThemeWrapper.getTheme().obtainStyledAttributes(attrsAppearance);
+
+ if (appearance != null) {
+ for (int i = 0; i < appearance.getIndexCount(); i++) {
+ int idx = appearance.getIndex(i);
+ int color = appearance.getColor(idx, 0);
+ result[idx] = color;
+ }
+ appearance.recycle();
+ }
+
+ return result;
+ }
+
+ @WrapForJNI
+ public static void killAnyZombies() {
+ GeckoProcessesVisitor visitor = new GeckoProcessesVisitor() {
+ @Override
+ public boolean callback(int pid) {
+ if (pid != android.os.Process.myPid())
+ android.os.Process.killProcess(pid);
+ return true;
+ }
+ };
+
+ EnumerateGeckoProcesses(visitor);
+ }
+
+ interface GeckoProcessesVisitor {
+ boolean callback(int pid);
+ }
+
+ private static void EnumerateGeckoProcesses(GeckoProcessesVisitor visiter) {
+ int pidColumn = -1;
+ int userColumn = -1;
+
+ try {
+ // run ps and parse its output
+ java.lang.Process ps = Runtime.getRuntime().exec("ps");
+ BufferedReader in = new BufferedReader(new InputStreamReader(ps.getInputStream()),
+ 2048);
+
+ String headerOutput = in.readLine();
+
+ // figure out the column offsets. We only care about the pid and user fields
+ StringTokenizer st = new StringTokenizer(headerOutput);
+
+ int tokenSoFar = 0;
+ while (st.hasMoreTokens()) {
+ String next = st.nextToken();
+ if (next.equalsIgnoreCase("PID"))
+ pidColumn = tokenSoFar;
+ else if (next.equalsIgnoreCase("USER"))
+ userColumn = tokenSoFar;
+ tokenSoFar++;
+ }
+
+ // alright, the rest are process entries.
+ String psOutput = null;
+ while ((psOutput = in.readLine()) != null) {
+ String[] split = psOutput.split("\\s+");
+ if (split.length <= pidColumn || split.length <= userColumn)
+ continue;
+ int uid = android.os.Process.getUidForName(split[userColumn]);
+ if (uid == android.os.Process.myUid() &&
+ !split[split.length - 1].equalsIgnoreCase("ps")) {
+ int pid = Integer.parseInt(split[pidColumn]);
+ boolean keepGoing = visiter.callback(pid);
+ if (keepGoing == false)
+ break;
+ }
+ }
+ in.close();
+ }
+ catch (Exception e) {
+ Log.w(LOGTAG, "Failed to enumerate Gecko processes.", e);
+ }
+ }
+
+ public static String getAppNameByPID(int pid) {
+ BufferedReader cmdlineReader = null;
+ String path = "/proc/" + pid + "/cmdline";
+ try {
+ File cmdlineFile = new File(path);
+ if (!cmdlineFile.exists())
+ return "";
+ cmdlineReader = new BufferedReader(new FileReader(cmdlineFile));
+ return cmdlineReader.readLine().trim();
+ } catch (Exception ex) {
+ return "";
+ } finally {
+ if (null != cmdlineReader) {
+ try {
+ cmdlineReader.close();
+ } catch (Exception e) { }
+ }
+ }
+ }
+
+ public static void listOfOpenFiles() {
+ int pidColumn = -1;
+ int nameColumn = -1;
+
+ try {
+ String filter = GeckoProfile.get(getApplicationContext()).getDir().toString();
+ Log.d(LOGTAG, "[OPENFILE] Filter: " + filter);
+
+ // run lsof and parse its output
+ java.lang.Process lsof = Runtime.getRuntime().exec("lsof");
+ BufferedReader in = new BufferedReader(new InputStreamReader(lsof.getInputStream()), 2048);
+
+ String headerOutput = in.readLine();
+ StringTokenizer st = new StringTokenizer(headerOutput);
+ int token = 0;
+ while (st.hasMoreTokens()) {
+ String next = st.nextToken();
+ if (next.equalsIgnoreCase("PID"))
+ pidColumn = token;
+ else if (next.equalsIgnoreCase("NAME"))
+ nameColumn = token;
+ token++;
+ }
+
+ // alright, the rest are open file entries.
+ Map<Integer, String> pidNameMap = new TreeMap<Integer, String>();
+ String output = null;
+ while ((output = in.readLine()) != null) {
+ String[] split = output.split("\\s+");
+ if (split.length <= pidColumn || split.length <= nameColumn)
+ continue;
+ final Integer pid = Integer.valueOf(split[pidColumn]);
+ String name = pidNameMap.get(pid);
+ if (name == null) {
+ name = getAppNameByPID(pid.intValue());
+ pidNameMap.put(pid, name);
+ }
+ String file = split[nameColumn];
+ if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(file) && file.startsWith(filter))
+ Log.d(LOGTAG, "[OPENFILE] " + name + "(" + split[pidColumn] + ") : " + file);
+ }
+ in.close();
+ } catch (Exception e) { }
+ }
+
+ @WrapForJNI(stubName = "GetIconForExtensionWrapper")
+ public static byte[] getIconForExtension(String aExt, int iconSize) {
+ try {
+ if (iconSize <= 0)
+ iconSize = 16;
+
+ if (aExt != null && aExt.length() > 1 && aExt.charAt(0) == '.')
+ aExt = aExt.substring(1);
+
+ PackageManager pm = getApplicationContext().getPackageManager();
+ Drawable icon = getDrawableForExtension(pm, aExt);
+ if (icon == null) {
+ // Use a generic icon
+ icon = pm.getDefaultActivityIcon();
+ }
+
+ Bitmap bitmap = ((BitmapDrawable)icon).getBitmap();
+ if (bitmap.getWidth() != iconSize || bitmap.getHeight() != iconSize)
+ bitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, true);
+
+ ByteBuffer buf = ByteBuffer.allocate(iconSize * iconSize * 4);
+ bitmap.copyPixelsToBuffer(buf);
+
+ return buf.array();
+ }
+ catch (Exception e) {
+ Log.w(LOGTAG, "getIconForExtension failed.", e);
+ return null;
+ }
+ }
+
+ public static String getMimeTypeFromExtension(String ext) {
+ final MimeTypeMap mtm = MimeTypeMap.getSingleton();
+ return mtm.getMimeTypeFromExtension(ext);
+ }
+
+ private static Drawable getDrawableForExtension(PackageManager pm, String aExt) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ final String mimeType = getMimeTypeFromExtension(aExt);
+ if (mimeType != null && mimeType.length() > 0)
+ intent.setType(mimeType);
+ else
+ return null;
+
+ List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+ if (list.size() == 0)
+ return null;
+
+ ResolveInfo resolveInfo = list.get(0);
+
+ if (resolveInfo == null)
+ return null;
+
+ ActivityInfo activityInfo = resolveInfo.activityInfo;
+
+ return activityInfo.loadIcon(pm);
+ }
+
+ @WrapForJNI
+ public static boolean getShowPasswordSetting() {
+ try {
+ int showPassword =
+ Settings.System.getInt(getApplicationContext().getContentResolver(),
+ Settings.System.TEXT_SHOW_PASSWORD, 1);
+ return (showPassword > 0);
+ }
+ catch (Exception e) {
+ return true;
+ }
+ }
+
+ @WrapForJNI(stubName = "AddPluginViewWrapper")
+ public static void addPluginView(View view,
+ float x, float y,
+ float w, float h,
+ boolean isFullScreen) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().addPluginView(view, new RectF(x, y, x + w, y + h), isFullScreen);
+ }
+
+ @WrapForJNI
+ public static void removePluginView(View view, boolean isFullScreen) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().removePluginView(view, isFullScreen);
+ }
+
+ /**
+ * A plugin that wish to be loaded in the WebView must provide this permission
+ * in their AndroidManifest.xml.
+ */
+ public static final String PLUGIN_ACTION = "android.webkit.PLUGIN";
+ public static final String PLUGIN_PERMISSION = "android.webkit.permission.PLUGIN";
+
+ private static final String PLUGIN_SYSTEM_LIB = "/system/lib/plugins/";
+
+ private static final String PLUGIN_TYPE = "type";
+ private static final String TYPE_NATIVE = "native";
+ public static final ArrayList<PackageInfo> mPackageInfoCache = new ArrayList<>();
+
+ // Returns null if plugins are blocked on the device.
+ static String[] getPluginDirectories() {
+
+ // Block on Pixel C.
+ if ((new File("/system/lib/hw/power.dragon.so")).exists()) {
+ Log.w(LOGTAG, "Blocking plugins because of Pixel C device (bug 1255122)");
+ return null;
+ }
+ // An awful hack to detect Tegra devices. Easiest way to do it without spinning up a EGL context.
+ boolean isTegra = (new File("/system/lib/hw/gralloc.tegra.so")).exists() ||
+ (new File("/system/lib/hw/gralloc.tegra3.so")).exists() ||
+ (new File("/sys/class/nvidia-gpu")).exists();
+ if (isTegra) {
+ // disable on KitKat (bug 957694)
+ if (Versions.feature19Plus) {
+ Log.w(LOGTAG, "Blocking plugins because of Tegra (bug 957694)");
+ return null;
+ }
+
+ // disable Flash on Tegra ICS with CM9 and other custom firmware (bug 736421)
+ final File vfile = new File("/proc/version");
+ try {
+ if (vfile.canRead()) {
+ final BufferedReader reader = new BufferedReader(new FileReader(vfile));
+ try {
+ final String version = reader.readLine();
+ if (version.indexOf("CM9") != -1 ||
+ version.indexOf("cyanogen") != -1 ||
+ version.indexOf("Nova") != -1) {
+ Log.w(LOGTAG, "Blocking plugins because of Tegra 2 + unofficial ICS bug (bug 736421)");
+ return null;
+ }
+ } finally {
+ reader.close();
+ }
+ }
+ } catch (IOException ex) {
+ // Do nothing.
+ }
+ }
+
+ ArrayList<String> directories = new ArrayList<String>();
+ PackageManager pm = getApplicationContext().getPackageManager();
+ List<ResolveInfo> plugins = pm.queryIntentServices(new Intent(PLUGIN_ACTION),
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+
+ synchronized (mPackageInfoCache) {
+
+ // clear the list of existing packageInfo objects
+ mPackageInfoCache.clear();
+
+
+ for (ResolveInfo info : plugins) {
+
+ // retrieve the plugin's service information
+ ServiceInfo serviceInfo = info.serviceInfo;
+ if (serviceInfo == null) {
+ Log.w(LOGTAG, "Ignoring bad plugin.");
+ continue;
+ }
+
+ // Blacklist HTC's flash lite.
+ // See bug #704516 - We're not quite sure what Flash Lite does,
+ // but loading it causes Flash to give errors and fail to draw.
+ if (serviceInfo.packageName.equals("com.htc.flashliteplugin")) {
+ Log.w(LOGTAG, "Skipping HTC's flash lite plugin");
+ continue;
+ }
+
+
+ // Retrieve information from the plugin's manifest.
+ PackageInfo pkgInfo;
+ try {
+ pkgInfo = pm.getPackageInfo(serviceInfo.packageName,
+ PackageManager.GET_PERMISSIONS
+ | PackageManager.GET_SIGNATURES);
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
+ continue;
+ }
+
+ if (pkgInfo == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Could not load package information.");
+ continue;
+ }
+
+ /*
+ * find the location of the plugin's shared library. The default
+ * is to assume the app is either a user installed app or an
+ * updated system app. In both of these cases the library is
+ * stored in the app's data directory.
+ */
+ String directory = pkgInfo.applicationInfo.dataDir + "/lib";
+ final int appFlags = pkgInfo.applicationInfo.flags;
+ final int updatedSystemFlags = ApplicationInfo.FLAG_SYSTEM |
+ ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
+
+ // preloaded system app with no user updates
+ if ((appFlags & updatedSystemFlags) == ApplicationInfo.FLAG_SYSTEM) {
+ directory = PLUGIN_SYSTEM_LIB + pkgInfo.packageName;
+ }
+
+ // check if the plugin has the required permissions
+ String permissions[] = pkgInfo.requestedPermissions;
+ if (permissions == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Does not have required permission.");
+ continue;
+ }
+ boolean permissionOk = false;
+ for (String permit : permissions) {
+ if (PLUGIN_PERMISSION.equals(permit)) {
+ permissionOk = true;
+ break;
+ }
+ }
+ if (!permissionOk) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Does not have required permission (2).");
+ continue;
+ }
+
+ // check to ensure the plugin is properly signed
+ Signature signatures[] = pkgInfo.signatures;
+ if (signatures == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Not signed.");
+ continue;
+ }
+
+ // determine the type of plugin from the manifest
+ if (serviceInfo.metaData == null) {
+ Log.e(LOGTAG, "The plugin '" + serviceInfo.name + "' has no defined type.");
+ continue;
+ }
+
+ String pluginType = serviceInfo.metaData.getString(PLUGIN_TYPE);
+ if (!TYPE_NATIVE.equals(pluginType)) {
+ Log.e(LOGTAG, "Unrecognized plugin type: " + pluginType);
+ continue;
+ }
+
+ try {
+ Class<?> cls = getPluginClass(serviceInfo.packageName, serviceInfo.name);
+
+ //TODO implement any requirements of the plugin class here!
+ boolean classFound = true;
+
+ if (!classFound) {
+ Log.e(LOGTAG, "The plugin's class' " + serviceInfo.name + "' does not extend the appropriate class.");
+ continue;
+ }
+
+ } catch (NameNotFoundException e) {
+ Log.e(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
+ continue;
+ } catch (ClassNotFoundException e) {
+ Log.e(LOGTAG, "Can't find plugin's class: " + serviceInfo.name);
+ continue;
+ }
+
+ // if all checks have passed then make the plugin available
+ mPackageInfoCache.add(pkgInfo);
+ directories.add(directory);
+ }
+ }
+
+ return directories.toArray(new String[directories.size()]);
+ }
+
+ static String getPluginPackage(String pluginLib) {
+
+ if (pluginLib == null || pluginLib.length() == 0) {
+ return null;
+ }
+
+ synchronized (mPackageInfoCache) {
+ for (PackageInfo pkgInfo : mPackageInfoCache) {
+ if (pluginLib.contains(pkgInfo.packageName)) {
+ return pkgInfo.packageName;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ static Class<?> getPluginClass(String packageName, String className)
+ throws NameNotFoundException, ClassNotFoundException {
+ Context pluginContext = getApplicationContext().createPackageContext(packageName,
+ Context.CONTEXT_INCLUDE_CODE |
+ Context.CONTEXT_IGNORE_SECURITY);
+ ClassLoader pluginCL = pluginContext.getClassLoader();
+ return pluginCL.loadClass(className);
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public static Class<?> loadPluginClass(String className, String libName) {
+ if (getGeckoInterface() == null)
+ return null;
+ try {
+ final String packageName = getPluginPackage(libName);
+ final int contextFlags = Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY;
+ final Context pluginContext = getApplicationContext().createPackageContext(
+ packageName, contextFlags);
+ return pluginContext.getClassLoader().loadClass(className);
+ } catch (java.lang.ClassNotFoundException cnfe) {
+ Log.w(LOGTAG, "Couldn't find plugin class " + className, cnfe);
+ return null;
+ } catch (android.content.pm.PackageManager.NameNotFoundException nnfe) {
+ Log.w(LOGTAG, "Couldn't find package.", nnfe);
+ return null;
+ }
+ }
+
+ private static Context sApplicationContext;
+ private static ContextGetter sContextGetter;
+
+ @Deprecated
+ @WrapForJNI(allowMultithread = true)
+ public static Context getContext() {
+ return sContextGetter.getContext();
+ }
+
+ public static void setContextGetter(ContextGetter cg) {
+ sContextGetter = cg;
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public static Context getApplicationContext() {
+ return sApplicationContext;
+ }
+
+ public static void setApplicationContext(final Context context) {
+ sApplicationContext = context;
+ }
+
+ public static SharedPreferences getSharedPreferences() {
+ if (sContextGetter == null) {
+ throw new IllegalStateException("No ContextGetter; cannot fetch prefs.");
+ }
+ return sContextGetter.getSharedPreferences();
+ }
+
+ public interface AppStateListener {
+ public void onPause();
+ public void onResume();
+ public void onOrientationChanged();
+ }
+
+ public interface GeckoInterface {
+ public GeckoProfile getProfile();
+ public Activity getActivity();
+ public String getDefaultUAString();
+ public LocationListener getLocationListener();
+ public SensorEventListener getSensorEventListener();
+ public void doRestart();
+ public void setFullScreen(boolean fullscreen);
+ public void addPluginView(View view, final RectF rect, final boolean isFullScreen);
+ public void removePluginView(final View view, final boolean isFullScreen);
+ public void enableCameraView();
+ public void disableCameraView();
+ public void addAppStateListener(AppStateListener listener);
+ public void removeAppStateListener(AppStateListener listener);
+ public View getCameraView();
+ public void notifyWakeLockChanged(String topic, String state);
+ public void onInputMethodChanged(String inputMethod);
+ public boolean areTabsShown();
+ public AbsoluteLayout getPluginContainer();
+ public void notifyCheckUpdateResult(String result);
+ public void invalidateOptionsMenu();
+
+ /**
+ * Create a shortcut -- generally a home-screen icon -- linking the given title to the given URI.
+ * <p>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param title of URI to link to.
+ * @param URI to link to.
+ */
+ public void createShortcut(String title, String URI);
+
+ /**
+ * Check if the given URI is visited.
+ * <p/>
+ * If it has been visited, call {@link GeckoAppShell#notifyUriVisited(String)}. (If it
+ * has not been visited, do nothing.)
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri to check.
+ */
+ public void checkUriVisited(String uri);
+
+ /**
+ * Mark the given URI as visited in Gecko.
+ * <p/>
+ * Implementors may maintain some local store of visited URIs in order to be able to
+ * answer {@link #checkUriVisited(String)} requests affirmatively.
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri to mark.
+ */
+ public void markUriVisited(final String uri);
+
+ /**
+ * Set the title of the given URI, as determined by Gecko.
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri given.
+ * @param title to associate with the given URI.
+ */
+ public void setUriTitle(final String uri, final String title);
+
+ public void setAccessibilityEnabled(boolean enabled);
+
+ public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title);
+
+ public String[] getHandlersForMimeType(String mimeType, String action);
+ public String[] getHandlersForURL(String url, String action);
+
+ /**
+ * URI of the underlying chrome window to be opened, or null to use the default GeckoView
+ * XUL container <tt>chrome://browser/content/geckoview.xul</tt>. See
+ * <a href="https://developer.mozilla.org/en/docs/toolkit.defaultChromeURI">https://developer.mozilla.org/en/docs/toolkit.defaultChromeURI</a>
+ *
+ * @return URI or null.
+ */
+ String getDefaultChromeURI();
+ };
+
+ private static GeckoInterface sGeckoInterface;
+
+ public static GeckoInterface getGeckoInterface() {
+ return sGeckoInterface;
+ }
+
+ public static void setGeckoInterface(GeckoInterface aGeckoInterface) {
+ sGeckoInterface = aGeckoInterface;
+ }
+
+ public static android.hardware.Camera sCamera;
+
+ static native void cameraCallbackBridge(byte[] data);
+
+ static final int kPreferredFPS = 25;
+ static byte[] sCameraBuffer;
+
+
+ @WrapForJNI(stubName = "InitCameraWrapper")
+ static int[] initCamera(String aContentType, int aCamera, int aWidth, int aHeight) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().enableCameraView();
+ } catch (Exception e) { }
+ }
+ });
+
+ // [0] = 0|1 (failure/success)
+ // [1] = width
+ // [2] = height
+ // [3] = fps
+ int[] result = new int[4];
+ result[0] = 0;
+
+ if (android.hardware.Camera.getNumberOfCameras() == 0) {
+ return result;
+ }
+
+ try {
+ sCamera = android.hardware.Camera.open(aCamera);
+
+ android.hardware.Camera.Parameters params = sCamera.getParameters();
+ params.setPreviewFormat(ImageFormat.NV21);
+
+ // use the preview fps closest to 25 fps.
+ int fpsDelta = 1000;
+ try {
+ Iterator<Integer> it = params.getSupportedPreviewFrameRates().iterator();
+ while (it.hasNext()) {
+ int nFps = it.next();
+ if (Math.abs(nFps - kPreferredFPS) < fpsDelta) {
+ fpsDelta = Math.abs(nFps - kPreferredFPS);
+ params.setPreviewFrameRate(nFps);
+ }
+ }
+ } catch (Exception e) {
+ params.setPreviewFrameRate(kPreferredFPS);
+ }
+
+ // set up the closest preview size available
+ Iterator<android.hardware.Camera.Size> sit = params.getSupportedPreviewSizes().iterator();
+ int sizeDelta = 10000000;
+ int bufferSize = 0;
+ while (sit.hasNext()) {
+ android.hardware.Camera.Size size = sit.next();
+ if (Math.abs(size.width * size.height - aWidth * aHeight) < sizeDelta) {
+ sizeDelta = Math.abs(size.width * size.height - aWidth * aHeight);
+ params.setPreviewSize(size.width, size.height);
+ bufferSize = size.width * size.height;
+ }
+ }
+
+ try {
+ if (getGeckoInterface() != null) {
+ View cameraView = getGeckoInterface().getCameraView();
+ if (cameraView instanceof SurfaceView) {
+ sCamera.setPreviewDisplay(((SurfaceView)cameraView).getHolder());
+ } else if (cameraView instanceof TextureView) {
+ sCamera.setPreviewTexture(((TextureView)cameraView).getSurfaceTexture());
+ }
+ }
+ } catch (IOException | RuntimeException e) {
+ Log.w(LOGTAG, "Error setPreviewXXX:", e);
+ }
+
+ sCamera.setParameters(params);
+ sCameraBuffer = new byte[(bufferSize * 12) / 8];
+ sCamera.addCallbackBuffer(sCameraBuffer);
+ sCamera.setPreviewCallbackWithBuffer(new android.hardware.Camera.PreviewCallback() {
+ @Override
+ public void onPreviewFrame(byte[] data, android.hardware.Camera camera) {
+ cameraCallbackBridge(data);
+ if (sCamera != null)
+ sCamera.addCallbackBuffer(sCameraBuffer);
+ }
+ });
+ sCamera.startPreview();
+ params = sCamera.getParameters();
+ result[0] = 1;
+ result[1] = params.getPreviewSize().width;
+ result[2] = params.getPreviewSize().height;
+ result[3] = params.getPreviewFrameRate();
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "initCamera RuntimeException.", e);
+ result[0] = result[1] = result[2] = result[3] = 0;
+ }
+ return result;
+ }
+
+ @WrapForJNI
+ static synchronized void closeCamera() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().disableCameraView();
+ } catch (Exception e) { }
+ }
+ });
+ if (sCamera != null) {
+ sCamera.stopPreview();
+ sCamera.release();
+ sCamera = null;
+ sCameraBuffer = null;
+ }
+ }
+
+ /*
+ * Battery API related methods.
+ */
+ @WrapForJNI
+ public static void enableBatteryNotifications() {
+ GeckoBatteryManager.enableNotifications();
+ }
+
+ @WrapForJNI(stubName = "HandleGeckoMessageWrapper")
+ public static void handleGeckoMessage(final NativeJSContainer message) {
+ EventDispatcher.getInstance().dispatchEvent(message);
+ message.disposeNative();
+ }
+
+ @WrapForJNI
+ public static void disableBatteryNotifications() {
+ GeckoBatteryManager.disableNotifications();
+ }
+
+ @WrapForJNI(stubName = "GetCurrentBatteryInformationWrapper")
+ public static double[] getCurrentBatteryInformation() {
+ return GeckoBatteryManager.getCurrentInformation();
+ }
+
+ @WrapForJNI(stubName = "CheckURIVisited")
+ static void checkUriVisited(String uri) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.checkUriVisited(uri);
+ }
+
+ @WrapForJNI(stubName = "MarkURIVisited")
+ static void markUriVisited(final String uri) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.markUriVisited(uri);
+ }
+
+ @WrapForJNI(stubName = "SetURITitle")
+ static void setUriTitle(final String uri, final String title) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.setUriTitle(uri, title);
+ }
+
+ @WrapForJNI
+ static void hideProgressDialog() {
+ // unused stub
+ }
+
+ /*
+ * WebSMS related methods.
+ */
+ public static void sendMessage(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify) {
+ if (!SmsManager.isEnabled()) {
+ return;
+ }
+
+ SmsManager.getInstance().send(aNumber, aMessage, aRequestId, aShouldNotify);
+ }
+
+ @WrapForJNI(stubName = "SendMessageWrapper")
+ public static void sendMessage(String aNumber, String aMessage, int aRequestId) {
+ sendMessage(aNumber, aMessage, aRequestId, /* shouldNotify */ true);
+ }
+
+ @WrapForJNI(stubName = "GetMessageWrapper")
+ public static void getMessage(int aMessageId, int aRequestId) {
+ if (!SmsManager.isEnabled()) {
+ return;
+ }
+
+ SmsManager.getInstance().getMessage(aMessageId, aRequestId);
+ }
+
+ @WrapForJNI(stubName = "DeleteMessageWrapper")
+ public static void deleteMessage(int aMessageId, int aRequestId) {
+ if (!SmsManager.isEnabled()) {
+ return;
+ }
+
+ SmsManager.getInstance().deleteMessage(aMessageId, aRequestId);
+ }
+
+ @WrapForJNI
+ public static void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId) {
+ if (!SmsManager.isEnabled()) {
+ return;
+ }
+
+ SmsManager.getInstance().markMessageRead(aMessageId, aValue, aSendReadReport, aRequestId);
+ }
+
+ @WrapForJNI(stubName = "CreateMessageCursorWrapper")
+ public static void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
+ if (!SmsManager.isEnabled()) {
+ return;
+ }
+
+ SmsManager.getInstance().createMessageCursor(aStartDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, aThreadId, aReverse, aRequestId);
+ }
+
+ @WrapForJNI(stubName = "GetNextMessageWrapper")
+ public static void getNextMessage(int aRequestId) {
+ if (!SmsManager.isEnabled()) {
+ return;
+ }
+
+ SmsManager.getInstance().getNextMessage(aRequestId);
+ }
+
+ @WrapForJNI(stubName = "CreateThreadCursorWrapper")
+ public static void createThreadCursor(int aRequestId) {
+ Log.i("GeckoAppShell", "CreateThreadCursorWrapper!");
+
+ if (!SmsManager.isEnabled()) {
+ return;
+ }
+
+ SmsManager.getInstance().createThreadCursor(aRequestId);
+ }
+
+ @WrapForJNI(stubName = "GetNextThreadWrapper")
+ public static void getNextThread(int aRequestId) {
+ if (!SmsManager.isEnabled()) {
+ return;
+ }
+
+ SmsManager.getInstance().getNextThread(aRequestId);
+ }
+
+ /* Called by JNI from AndroidBridge, and by reflection from tests/BaseTest.java.in */
+ @WrapForJNI
+ @RobocopTarget
+ public static boolean isTablet() {
+ return HardwareUtils.isTablet();
+ }
+
+ private static boolean sImeWasEnabledOnLastResize = false;
+ public static void viewSizeChanged() {
+ GeckoView v = (GeckoView) getLayerView();
+ if (v == null) {
+ return;
+ }
+ boolean imeIsEnabled = v.isIMEEnabled();
+ if (imeIsEnabled && !sImeWasEnabledOnLastResize) {
+ // The IME just came up after not being up, so let's scroll
+ // to the focused input.
+ notifyObservers("ScrollTo:FocusedInput", "");
+ }
+ sImeWasEnabledOnLastResize = imeIsEnabled;
+ }
+
+ @WrapForJNI(stubName = "GetCurrentNetworkInformationWrapper")
+ public static double[] getCurrentNetworkInformation() {
+ return GeckoNetworkManager.getInstance().getCurrentInformation();
+ }
+
+ @WrapForJNI
+ public static void enableNetworkNotifications() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoNetworkManager.getInstance().enableNotifications();
+ }
+ });
+ }
+
+ @WrapForJNI
+ public static void disableNetworkNotifications() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoNetworkManager.getInstance().disableNotifications();
+ }
+ });
+ }
+
+ @WrapForJNI(stubName = "GetScreenOrientationWrapper")
+ public static short getScreenOrientation() {
+ return GeckoScreenOrientation.getInstance().getScreenOrientation().value;
+ }
+
+ @WrapForJNI
+ public static int getScreenAngle() {
+ return GeckoScreenOrientation.getInstance().getAngle();
+ }
+
+ @WrapForJNI
+ public static void enableScreenOrientationNotifications() {
+ GeckoScreenOrientation.getInstance().enableNotifications();
+ }
+
+ @WrapForJNI
+ public static void disableScreenOrientationNotifications() {
+ GeckoScreenOrientation.getInstance().disableNotifications();
+ }
+
+ @WrapForJNI
+ public static void lockScreenOrientation(int aOrientation) {
+ GeckoScreenOrientation.getInstance().lock(aOrientation);
+ }
+
+ @WrapForJNI
+ public static void unlockScreenOrientation() {
+ GeckoScreenOrientation.getInstance().unlock();
+ }
+
+ @WrapForJNI
+ public static void notifyWakeLockChanged(String topic, String state) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().notifyWakeLockChanged(topic, state);
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public static void registerSurfaceTextureFrameListener(Object surfaceTexture, final int id) {
+ ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
+ @Override
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ GeckoAppShell.onSurfaceTextureFrameAvailable(surfaceTexture, id);
+ }
+ });
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public static void unregisterSurfaceTextureFrameListener(Object surfaceTexture) {
+ ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(null);
+ }
+
+ @WrapForJNI
+ public static boolean unlockProfile() {
+ // Try to kill any zombie Fennec's that might be running
+ GeckoAppShell.killAnyZombies();
+
+ // Then force unlock this profile
+ if (getGeckoInterface() != null) {
+ GeckoProfile profile = getGeckoInterface().getProfile();
+ File lock = profile.getFile(".parentlock");
+ return lock.exists() && lock.delete();
+ }
+ return false;
+ }
+
+ @WrapForJNI(stubName = "GetProxyForURIWrapper")
+ public static String getProxyForURI(String spec, String scheme, String host, int port) {
+ final ProxySelector ps = new ProxySelector();
+
+ Proxy proxy = ps.select(scheme, host);
+ if (Proxy.NO_PROXY.equals(proxy)) {
+ return "DIRECT";
+ }
+
+ switch (proxy.type()) {
+ case HTTP:
+ return "PROXY " + proxy.address().toString();
+ case SOCKS:
+ return "SOCKS " + proxy.address().toString();
+ }
+
+ return "DIRECT";
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ static InputStream createInputStream(URLConnection connection) throws IOException {
+ return connection.getInputStream();
+ }
+
+ private static class BitmapConnection extends URLConnection {
+ private Bitmap bitmap;
+
+ BitmapConnection(Bitmap b) throws MalformedURLException, IOException {
+ super(null);
+ bitmap = b;
+ }
+
+ @Override
+ public void connect() {}
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return new BitmapInputStream();
+ }
+
+ @Override
+ public String getContentType() {
+ return "image/png";
+ }
+
+ private final class BitmapInputStream extends PipedInputStream {
+ private boolean mHaveConnected = false;
+
+ @Override
+ public synchronized int read(byte[] buffer, int byteOffset, int byteCount)
+ throws IOException {
+ if (mHaveConnected) {
+ return super.read(buffer, byteOffset, byteCount);
+ }
+
+ final PipedOutputStream output = new PipedOutputStream();
+ connect(output);
+ ThreadUtils.postToBackgroundThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, output);
+ output.close();
+ } catch (IOException ioe) { }
+ }
+ });
+ mHaveConnected = true;
+ return super.read(buffer, byteOffset, byteCount);
+ }
+ }
+ }
+
+ @WrapForJNI(allowMultithread = true, narrowChars = true)
+ static URLConnection getConnection(String url) {
+ try {
+ String spec;
+ if (url.startsWith("android://")) {
+ spec = url.substring(10);
+ } else {
+ spec = url.substring(8);
+ }
+
+ // Check if we are loading a package icon.
+ try {
+ if (spec.startsWith("icon/")) {
+ String[] splits = spec.split("/");
+ if (splits.length != 2) {
+ return null;
+ }
+ final String pkg = splits[1];
+ final PackageManager pm = getApplicationContext().getPackageManager();
+ final Drawable d = pm.getApplicationIcon(pkg);
+ final Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(d);
+ return new BitmapConnection(bitmap);
+ }
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error", ex);
+ }
+
+ // if the colon got stripped, put it back
+ int colon = spec.indexOf(':');
+ if (colon == -1 || colon > spec.indexOf('/')) {
+ spec = spec.replaceFirst("/", ":/");
+ }
+ } catch (Exception ex) {
+ return null;
+ }
+ return null;
+ }
+
+ @WrapForJNI(allowMultithread = true, narrowChars = true)
+ static String connectionGetMimeType(URLConnection connection) {
+ return connection.getContentType();
+ }
+
+ /**
+ * Retrieve the absolute path of an external storage directory.
+ *
+ * @param type The type of directory to return
+ * @return Absolute path of the specified directory or null on failure
+ */
+ @WrapForJNI
+ static String getExternalPublicDirectory(final String type) {
+ final String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state) &&
+ !Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+ // External storage is not available.
+ return null;
+ }
+
+ if ("sdcard".equals(type)) {
+ // SD card has a separate path.
+ return Environment.getExternalStorageDirectory().getAbsolutePath();
+ }
+
+ final String systemType;
+ if ("downloads".equals(type)) {
+ systemType = Environment.DIRECTORY_DOWNLOADS;
+ } else if ("pictures".equals(type)) {
+ systemType = Environment.DIRECTORY_PICTURES;
+ } else if ("videos".equals(type)) {
+ systemType = Environment.DIRECTORY_MOVIES;
+ } else if ("music".equals(type)) {
+ systemType = Environment.DIRECTORY_MUSIC;
+ } else if ("apps".equals(type)) {
+ File appInternalStorageDirectory = getApplicationContext().getFilesDir();
+ return new File(appInternalStorageDirectory, "mozilla").getAbsolutePath();
+ } else {
+ return null;
+ }
+ return Environment.getExternalStoragePublicDirectory(systemType).getAbsolutePath();
+ }
+
+ @WrapForJNI
+ static int getMaxTouchPoints() {
+ PackageManager pm = getApplicationContext().getPackageManager();
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND)) {
+ // at least, 5+ fingers.
+ return 5;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
+ // at least, 2+ fingers.
+ return 2;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) {
+ // 2 fingers
+ return 2;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
+ // 1 finger
+ return 1;
+ }
+ return 0;
+ }
+
+ public static synchronized void resetScreenSize() {
+ sScreenSize = null;
+ }
+
+ @WrapForJNI
+ public static synchronized Rect getScreenSize() {
+ if (sScreenSize == null) {
+ final WindowManager wm = (WindowManager)
+ getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ final Display disp = wm.getDefaultDisplay();
+ sScreenSize = new Rect(0, 0, disp.getWidth(), disp.getHeight());
+ }
+ return sScreenSize;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
@@ -0,0 +1,196 @@
+/* -*- 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+
+public class GeckoBatteryManager extends BroadcastReceiver {
+ private static final String LOGTAG = "GeckoBatteryManager";
+
+ // Those constants should be keep in sync with the ones in:
+ // dom/battery/Constants.h
+ private final static double kDefaultLevel = 1.0;
+ private final static boolean kDefaultCharging = true;
+ private final static double kDefaultRemainingTime = 0.0;
+ private final static double kUnknownRemainingTime = -1.0;
+
+ private static long sLastLevelChange;
+ private static boolean sNotificationsEnabled;
+ private static double sLevel = kDefaultLevel;
+ private static boolean sCharging = kDefaultCharging;
+ private static double sRemainingTime = kDefaultRemainingTime;
+
+ private static final GeckoBatteryManager sInstance = new GeckoBatteryManager();
+
+ private final IntentFilter mFilter;
+ private Context mApplicationContext;
+ private boolean mIsEnabled;
+
+ public static GeckoBatteryManager getInstance() {
+ return sInstance;
+ }
+
+ private GeckoBatteryManager() {
+ mFilter = new IntentFilter();
+ mFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ }
+
+ public synchronized void start(final Context context) {
+ if (mIsEnabled) {
+ Log.w(LOGTAG, "Already started!");
+ return;
+ }
+
+ mApplicationContext = context.getApplicationContext();
+ // registerReceiver will return null if registering fails.
+ if (mApplicationContext.registerReceiver(this, mFilter) == null) {
+ Log.e(LOGTAG, "Registering receiver failed");
+ } else {
+ mIsEnabled = true;
+ }
+ }
+
+ public synchronized void stop() {
+ if (!mIsEnabled) {
+ Log.w(LOGTAG, "Already stopped!");
+ return;
+ }
+
+ mApplicationContext.unregisterReceiver(this);
+ mApplicationContext = null;
+ mIsEnabled = false;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
+ Log.e(LOGTAG, "Got an unexpected intent!");
+ return;
+ }
+
+ boolean previousCharging = isCharging();
+ double previousLevel = getLevel();
+
+ // NOTE: it might not be common (in 2012) but technically, Android can run
+ // on a device that has no battery so we want to make sure it's not the case
+ // before bothering checking for battery state.
+ // However, the Galaxy Nexus phone advertises itself as battery-less which
+ // force us to special-case the logic.
+ // See the Google bug: https://code.google.com/p/android/issues/detail?id=22035
+ if (intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false) ||
+ Build.MODEL.equals("Galaxy Nexus")) {
+ int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
+ if (plugged == -1) {
+ sCharging = kDefaultCharging;
+ Log.e(LOGTAG, "Failed to get the plugged status!");
+ } else {
+ // Likely, if plugged > 0, it's likely plugged and charging but the doc
+ // isn't clear about that.
+ sCharging = plugged != 0;
+ }
+
+ if (sCharging != previousCharging) {
+ sRemainingTime = kUnknownRemainingTime;
+ // The new remaining time is going to take some time to show up but
+ // it's the best way to show a not too wrong value.
+ sLastLevelChange = 0;
+ }
+
+ // We need two doubles because sLevel is a double.
+ double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ if (current == -1 || max == -1) {
+ Log.e(LOGTAG, "Failed to get battery level!");
+ sLevel = kDefaultLevel;
+ } else {
+ sLevel = current / max;
+ }
+
+ if (sLevel == 1.0 && sCharging) {
+ sRemainingTime = kDefaultRemainingTime;
+ } else if (sLevel != previousLevel) {
+ // Estimate remaining time.
+ if (sLastLevelChange != 0) {
+ // Use elapsedRealtime() because we want to track time across device sleeps.
+ long currentTime = SystemClock.elapsedRealtime();
+ long dt = (currentTime - sLastLevelChange) / 1000;
+ double dLevel = sLevel - previousLevel;
+
+ if (sCharging) {
+ if (dLevel < 0) {
+ sRemainingTime = kUnknownRemainingTime;
+ } else {
+ sRemainingTime = Math.round(dt / dLevel * (1.0 - sLevel));
+ }
+ } else {
+ if (dLevel > 0) {
+ Log.w(LOGTAG, "When discharging, level should decrease!");
+ sRemainingTime = kUnknownRemainingTime;
+ } else {
+ sRemainingTime = Math.round(dt / -dLevel * sLevel);
+ }
+ }
+
+ sLastLevelChange = currentTime;
+ } else {
+ // That's the first time we got an update, we can't do anything.
+ sLastLevelChange = SystemClock.elapsedRealtime();
+ }
+ }
+ } else {
+ sLevel = kDefaultLevel;
+ sCharging = kDefaultCharging;
+ sRemainingTime = kDefaultRemainingTime;
+ }
+
+ /*
+ * We want to inform listeners if the following conditions are fulfilled:
+ * - we have at least one observer;
+ * - the charging state or the level has changed.
+ *
+ * Note: no need to check for a remaining time change given that it's only
+ * updated if there is a level change or a charging change.
+ *
+ * The idea is to prevent doing all the way to the DOM code in the child
+ * process to finally not send an event.
+ */
+ if (sNotificationsEnabled &&
+ (previousCharging != isCharging() || previousLevel != getLevel())) {
+ GeckoAppShell.notifyBatteryChange(getLevel(), isCharging(), getRemainingTime());
+ }
+ }
+
+ public static boolean isCharging() {
+ return sCharging;
+ }
+
+ public static double getLevel() {
+ return sLevel;
+ }
+
+ public static double getRemainingTime() {
+ return sRemainingTime;
+ }
+
+ public static void enableNotifications() {
+ sNotificationsEnabled = true;
+ }
+
+ public static void disableNotifications() {
+ sNotificationsEnabled = false;
+ }
+
+ public static double[] getCurrentInformation() {
+ return new double[] { getLevel(), isCharging() ? 1.0 : 0.0, getRemainingTime() };
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
@@ -0,0 +1,1439 @@
+/* -*- 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.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Semaphore;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.CharacterStyle;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+
+/*
+ GeckoEditable implements only some functions of Editable
+ The field mText contains the actual underlying
+ SpannableStringBuilder/Editable that contains our text.
+*/
+final class GeckoEditable extends JNIObject
+ implements InvocationHandler, Editable,
+ GeckoEditableClient, GeckoEditableListener, GeckoEventListener {
+
+ private static final boolean DEBUG = false;
+ private static final String LOGTAG = "GeckoEditable";
+
+ // Filters to implement Editable's filtering functionality
+ private InputFilter[] mFilters;
+
+ private final SpannableStringBuilder mText;
+ private final Editable mProxy;
+ private final ActionQueue mActionQueue;
+
+ // mIcRunHandler is the Handler that currently runs Gecko-to-IC Runnables
+ // mIcPostHandler is the Handler to post Gecko-to-IC Runnables to
+ // The two can be different when switching from one handler to another
+ private Handler mIcRunHandler;
+ private Handler mIcPostHandler;
+
+ /* package */ GeckoEditableListener mListener;
+ /* package */ GeckoView mView;
+ /* package */ boolean mInBatchMode; // Used by IC thread
+ /* package */ boolean mNeedCompositionUpdate; // Used by IC thread
+ private boolean mFocused; // Used by IC thread
+ private boolean mGeckoFocused; // Used by Gecko thread
+ private boolean mIgnoreSelectionChange; // Used by Gecko thread
+ private volatile boolean mSuppressCompositions;
+ private volatile boolean mSuppressKeyUp;
+
+ private static final int IME_RANGE_CARETPOSITION = 1;
+ private static final int IME_RANGE_RAWINPUT = 2;
+ private static final int IME_RANGE_SELECTEDRAWTEXT = 3;
+ private static final int IME_RANGE_CONVERTEDTEXT = 4;
+ private static final int IME_RANGE_SELECTEDCONVERTEDTEXT = 5;
+
+ private static final int IME_RANGE_LINE_NONE = 0;
+ private static final int IME_RANGE_LINE_DOTTED = 1;
+ private static final int IME_RANGE_LINE_DASHED = 2;
+ private static final int IME_RANGE_LINE_SOLID = 3;
+ private static final int IME_RANGE_LINE_DOUBLE = 4;
+ private static final int IME_RANGE_LINE_WAVY = 5;
+
+ private static final int IME_RANGE_UNDERLINE = 1;
+ private static final int IME_RANGE_FORECOLOR = 2;
+ private static final int IME_RANGE_BACKCOLOR = 4;
+ private static final int IME_RANGE_LINECOLOR = 8;
+
+ @WrapForJNI
+ private native void onKeyEvent(int action, int keyCode, int scanCode, int metaState,
+ long time, int unicodeChar, int baseUnicodeChar,
+ int domPrintableKeyValue, int repeatCount, int flags,
+ boolean isSynthesizedImeKey, KeyEvent event);
+
+ private void onKeyEvent(KeyEvent event, int action, int savedMetaState,
+ boolean isSynthesizedImeKey) {
+ // Use a separate action argument so we can override the key's original action,
+ // e.g. change ACTION_MULTIPLE to ACTION_DOWN. That way we don't have to allocate
+ // a new key event just to change its action field.
+ //
+ // Normally we expect event.getMetaState() to reflect the current meta-state; however,
+ // some software-generated key events may not have event.getMetaState() set, e.g. key
+ // events from Swype. Therefore, it's necessary to combine the key's meta-states
+ // with the meta-states that we keep separately in KeyListener
+ final int metaState = event.getMetaState() | savedMetaState;
+ final int unmodifiedMetaState = metaState &
+ ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK);
+ final int unicodeChar = event.getUnicodeChar(metaState);
+ final int domPrintableKeyValue =
+ unicodeChar >= ' ' ? unicodeChar :
+ unmodifiedMetaState != metaState ? event.getUnicodeChar(unmodifiedMetaState) :
+ 0;
+ onKeyEvent(action, event.getKeyCode(), event.getScanCode(),
+ metaState, event.getEventTime(), unicodeChar,
+ // e.g. for Ctrl+A, Android returns 0 for unicodeChar,
+ // but Gecko expects 'a', so we return that in baseUnicodeChar.
+ event.getUnicodeChar(0), domPrintableKeyValue, event.getRepeatCount(),
+ event.getFlags(), isSynthesizedImeKey, event);
+ }
+
+ @WrapForJNI
+ private native void onImeSynchronize();
+
+ @WrapForJNI
+ private native void onImeAcknowledgeFocus();
+
+ @WrapForJNI
+ private native void onImeReplaceText(int start, int end, String text);
+
+ @WrapForJNI
+ private native void onImeAddCompositionRange(int start, int end, int rangeType,
+ int rangeStyles, int rangeLineStyle,
+ boolean rangeBoldLine, int rangeForeColor,
+ int rangeBackColor, int rangeLineColor);
+
+ @WrapForJNI
+ private native void onImeUpdateComposition(int start, int end);
+
+ /* An action that alters the Editable
+
+ Each action corresponds to a Gecko event. While the Gecko event is being sent to the Gecko
+ thread, the action stays on top of mActions queue. After the Gecko event is processed and
+ replied, the action is removed from the queue
+ */
+ private static final class Action {
+ // For input events (keypress, etc.); use with onImeSynchronize
+ static final int TYPE_EVENT = 0;
+ // For Editable.replace() call; use with onImeReplaceText
+ static final int TYPE_REPLACE_TEXT = 1;
+ // For Editable.setSpan() call; use with onImeSynchronize
+ static final int TYPE_SET_SPAN = 2;
+ // For Editable.removeSpan() call; use with onImeSynchronize
+ static final int TYPE_REMOVE_SPAN = 3;
+ // For focus events (in notifyIME); use with onImeAcknowledgeFocus
+ static final int TYPE_ACKNOWLEDGE_FOCUS = 4;
+ // For switching handler; use with onImeSynchronize
+ static final int TYPE_SET_HANDLER = 5;
+ // For updating composition; use with onImeUpdateComposition
+ static final int TYPE_UPDATE_COMPOSITION = 6;
+
+ final int mType;
+ boolean mUpdateComposition;
+ int mStart;
+ int mEnd;
+ CharSequence mSequence;
+ Object mSpanObject;
+ int mSpanFlags;
+ Handler mHandler;
+
+ Action(int type) {
+ mType = type;
+ }
+
+ static Action newReplaceText(CharSequence text, int start, int end) {
+ if (start < 0 || start > end) {
+ Log.e(LOGTAG, "invalid replace text offsets: " + start + " to " + end);
+ throw new IllegalArgumentException("invalid replace text offsets");
+ }
+
+ final Action action = new Action(TYPE_REPLACE_TEXT);
+ action.mSequence = text;
+ action.mStart = start;
+ action.mEnd = end;
+ return action;
+ }
+
+ static Action newSetSpan(Object object, int start, int end, int flags, boolean update) {
+ if (start < 0 || start > end) {
+ Log.e(LOGTAG, "invalid span offsets: " + start + " to " + end);
+ throw new IllegalArgumentException("invalid span offsets");
+ }
+ final Action action = new Action(TYPE_SET_SPAN);
+ action.mSpanObject = object;
+ action.mStart = start;
+ action.mEnd = end;
+ action.mSpanFlags = flags;
+ action.mUpdateComposition = update;
+ return action;
+ }
+
+ static Action newRemoveSpan(Object object, boolean update) {
+ final Action action = new Action(TYPE_REMOVE_SPAN);
+ action.mSpanObject = object;
+ action.mUpdateComposition = update;
+ return action;
+ }
+
+ static Action newSetHandler(Handler handler) {
+ final Action action = new Action(TYPE_SET_HANDLER);
+ action.mHandler = handler;
+ return action;
+ }
+
+ static Action newUpdateComposition(int start, int end) {
+ final Action action = new Action(TYPE_UPDATE_COMPOSITION);
+ action.mStart = start;
+ action.mEnd = end;
+ return action;
+ }
+ }
+
+ /* Queue of editing actions sent to Gecko thread that
+ the Gecko thread has not responded to yet */
+ private final class ActionQueue {
+ private final ConcurrentLinkedQueue<Action> mActions;
+ private final Semaphore mActionsActive;
+ private KeyCharacterMap mKeyMap;
+
+ ActionQueue() {
+ mActions = new ConcurrentLinkedQueue<Action>();
+ mActionsActive = new Semaphore(1);
+ }
+
+ void offer(Action action) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "offer: Action(" +
+ getConstantName(Action.class, "TYPE_", action.mType) + ")");
+ }
+
+ if (mListener == null) {
+ // We haven't initialized or we've been destroyed.
+ return;
+ }
+
+ if (mActions.isEmpty()) {
+ mActionsActive.acquireUninterruptibly();
+ mActions.offer(action);
+ } else synchronized (this) {
+ // tryAcquire here in case Gecko thread has just released it
+ mActionsActive.tryAcquire();
+ mActions.offer(action);
+ }
+
+ switch (action.mType) {
+ case Action.TYPE_EVENT:
+ case Action.TYPE_SET_SPAN:
+ case Action.TYPE_REMOVE_SPAN:
+ case Action.TYPE_SET_HANDLER:
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_REPLACE_TEXT:
+ // Because we get composition styling here essentially for free,
+ // we don't need to check if we're in batch mode.
+ if (icMaybeSendComposition(
+ action.mSequence, /* useEntireText */ true, /* notifyGecko */ false)) {
+ mNeedCompositionUpdate = false;
+ } else {
+ // Since we don't have a composition, we can try sending key events.
+ sendCharKeyEvents(action);
+ }
+ onImeReplaceText(action.mStart, action.mEnd, action.mSequence.toString());
+ break;
+
+ case Action.TYPE_ACKNOWLEDGE_FOCUS:
+ onImeAcknowledgeFocus();
+ break;
+
+ case Action.TYPE_UPDATE_COMPOSITION:
+ onImeUpdateComposition(action.mStart, action.mEnd);
+ break;
+
+ default:
+ throw new IllegalStateException("Action not processed");
+ }
+ }
+
+ private KeyEvent [] synthesizeKeyEvents(CharSequence cs) {
+ try {
+ if (mKeyMap == null) {
+ mKeyMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+ }
+ } catch (Exception e) {
+ // KeyCharacterMap.UnavailableException is not found on Gingerbread;
+ // besides, it seems like HC and ICS will throw something other than
+ // KeyCharacterMap.UnavailableException; so use a generic Exception here
+ return null;
+ }
+ KeyEvent [] keyEvents = mKeyMap.getEvents(cs.toString().toCharArray());
+ if (keyEvents == null || keyEvents.length == 0) {
+ return null;
+ }
+ return keyEvents;
+ }
+
+ private void sendCharKeyEvents(Action action) {
+ if (action.mSequence.length() != 1 ||
+ (action.mSequence instanceof Spannable &&
+ ((Spannable)action.mSequence).nextSpanTransition(
+ -1, Integer.MAX_VALUE, null) < Integer.MAX_VALUE)) {
+ // Spans are not preserved when we use key events,
+ // so we need the sequence to not have any spans
+ return;
+ }
+ KeyEvent [] keyEvents = synthesizeKeyEvents(action.mSequence);
+ if (keyEvents == null) {
+ return;
+ }
+ for (KeyEvent event : keyEvents) {
+ if (KeyEvent.isModifierKey(event.getKeyCode())) {
+ continue;
+ }
+ if (event.getAction() == KeyEvent.ACTION_UP && mSuppressKeyUp) {
+ continue;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "sending: " + event);
+ }
+ onKeyEvent(event, event.getAction(),
+ /* metaState */ 0, /* isSynthesizedImeKey */ true);
+ }
+ }
+
+ /**
+ * Remove the head of the queue. Throw if queue is empty.
+ */
+ void poll() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ if (mActions.poll() == null) {
+ throw new IllegalStateException("empty actions queue");
+ }
+
+ synchronized (this) {
+ if (mActions.isEmpty()) {
+ mActionsActive.release();
+ }
+ }
+ }
+
+ /**
+ * Return, but don't remove, the head of the queue, or null if queue is empty.
+ *
+ * @return head of the queue or null if empty.
+ */
+ Action peek() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ return mActions.peek();
+ }
+
+ void syncWithGecko() {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ if (mFocused && !mActions.isEmpty()) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "syncWithGecko blocking on thread " +
+ Thread.currentThread().getName());
+ }
+ mActionsActive.acquireUninterruptibly();
+ mActionsActive.release();
+ } else if (DEBUG && !mFocused) {
+ Log.d(LOGTAG, "skipped syncWithGecko (no focus)");
+ }
+ }
+
+ boolean isEmpty() {
+ return mActions.isEmpty();
+ }
+ }
+
+ @WrapForJNI
+ GeckoEditable(final GeckoView v) {
+ if (DEBUG) {
+ // Called by nsWindow.
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mActionQueue = new ActionQueue();
+
+ mText = new SpannableStringBuilder();
+
+ final Class<?>[] PROXY_INTERFACES = { Editable.class };
+ mProxy = (Editable)Proxy.newProxyInstance(
+ Editable.class.getClassLoader(),
+ PROXY_INTERFACES, this);
+
+ mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler();
+
+ onViewChange(v);
+ }
+
+ @WrapForJNI @Override
+ protected native void disposeNative();
+
+ @WrapForJNI
+ /* package */ void onViewChange(final GeckoView v) {
+ if (DEBUG) {
+ // Called by nsWindow.
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "onViewChange(" + v + ")");
+ }
+
+ final GeckoEditableListener newListener =
+ v != null ? GeckoInputConnection.create(v, this) : null;
+
+ final Runnable setListenerRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onViewChange (set listener)");
+ }
+
+ if (newListener != null) {
+ // Make sure there are no other things going on.
+ mActionQueue.syncWithGecko();
+ mListener = newListener;
+ } else {
+ // We're being destroyed. By this point, we should have cleared all
+ // pending Runnables on the IC thread, so it's safe to call
+ // disposeNative here.
+ mListener = null;
+ GeckoEditable.this.disposeNative();
+ }
+ }
+ };
+
+ // Post to UI thread first to make sure any code that is using the old input
+ // connection has finished running, before we switch to a new input connection or
+ // before we clear the input connection on destruction.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onViewChange (set IC)");
+ }
+
+ if (mView != null) {
+ // Detach the previous view.
+ mView.setInputConnectionListener(null);
+ }
+ if (v != null) {
+ // And attach the new view.
+ v.setInputConnectionListener((InputConnectionListener) newListener);
+ }
+
+ mView = v;
+ mIcPostHandler.post(setListenerRunnable);
+ }
+ });
+ }
+
+ private boolean onIcThread() {
+ return mIcRunHandler.getLooper() == Looper.myLooper();
+ }
+
+ private void assertOnIcThread() {
+ ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW);
+ }
+
+ private void geckoPostToIc(Runnable runnable) {
+ mIcPostHandler.post(runnable);
+ }
+
+ private Object getField(Object obj, String field, Object def) {
+ try {
+ return obj.getClass().getField(field).get(obj);
+ } catch (Exception e) {
+ return def;
+ }
+ }
+
+ /**
+ * Send composition ranges to Gecko if the text has composing spans.
+ *
+ * @param sequence Text with possible composing spans
+ * @param useEntireText If text has composing spans, treat the entire text as
+ * a Gecko composition, instead of just the spanned part.
+ * @param notifyGecko Notify Gecko of the new composition ranges;
+ * otherwise, the caller is responsible for notifying Gecko.
+ * @return Whether there was a composition
+ */
+ private boolean icMaybeSendComposition(final CharSequence sequence,
+ final boolean useEntireText,
+ final boolean notifyGecko) {
+ int selStart = Selection.getSelectionStart(sequence);
+ int selEnd = Selection.getSelectionEnd(sequence);
+
+ if (sequence instanceof Spanned) {
+ final Spanned text = (Spanned) sequence;
+ final Object[] spans = text.getSpans(0, text.length(), Object.class);
+ boolean found = false;
+ int composingStart = useEntireText ? 0 : Integer.MAX_VALUE;
+ int composingEnd = useEntireText ? text.length() : 0;
+
+ for (Object span : spans) {
+ if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) == 0) {
+ continue;
+ }
+ if (!useEntireText) {
+ composingStart = Math.min(composingStart, text.getSpanStart(span));
+ composingEnd = Math.max(composingEnd, text.getSpanEnd(span));
+ }
+ found = true;
+ }
+
+ if (useEntireText && (selStart < 0 || selEnd < 0)) {
+ selStart = composingEnd;
+ selEnd = composingEnd;
+ }
+
+ if (found) {
+ icSendComposition(text, selStart, selEnd, composingStart, composingEnd);
+ if (notifyGecko) {
+ mActionQueue.offer(Action.newUpdateComposition(
+ composingStart, composingEnd));
+ }
+ return true;
+ }
+ }
+
+ if (notifyGecko) {
+ // Set the selection by using a composition without ranges
+ mActionQueue.offer(Action.newUpdateComposition(selStart, selEnd));
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "icSendComposition(): no composition");
+ }
+ return false;
+ }
+
+ private void icSendComposition(final Spanned text,
+ final int selStart, final int selEnd,
+ final int composingStart, final int composingEnd) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "icSendComposition(\"" + text + "\", " +
+ composingStart + ", " + composingEnd + ")");
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd);
+ Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd);
+ }
+
+ if (selEnd >= composingStart && selEnd <= composingEnd) {
+ onImeAddCompositionRange(
+ selEnd - composingStart, selEnd - composingStart,
+ IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0);
+ }
+
+ int rangeStart = composingStart;
+ TextPaint tp = new TextPaint();
+ TextPaint emptyTp = new TextPaint();
+ // set initial foreground color to 0, because we check for tp.getColor() == 0
+ // below to decide whether to pass a foreground color to Gecko
+ emptyTp.setColor(0);
+ do {
+ int rangeType, rangeStyles = 0, rangeLineStyle = IME_RANGE_LINE_NONE;
+ boolean rangeBoldLine = false;
+ int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0;
+ int rangeEnd = text.nextSpanTransition(rangeStart, composingEnd, Object.class);
+
+ if (selStart > rangeStart && selStart < rangeEnd) {
+ rangeEnd = selStart;
+ } else if (selEnd > rangeStart && selEnd < rangeEnd) {
+ rangeEnd = selEnd;
+ }
+ CharacterStyle[] styleSpans =
+ text.getSpans(rangeStart, rangeEnd, CharacterStyle.class);
+
+ if (DEBUG) {
+ Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " +
+ rangeStart + "-" + rangeEnd);
+ }
+
+ if (styleSpans.length == 0) {
+ rangeType = (selStart == rangeStart && selEnd == rangeEnd)
+ ? IME_RANGE_SELECTEDRAWTEXT
+ : IME_RANGE_RAWINPUT;
+ } else {
+ rangeType = (selStart == rangeStart && selEnd == rangeEnd)
+ ? IME_RANGE_SELECTEDCONVERTEDTEXT
+ : IME_RANGE_CONVERTEDTEXT;
+ tp.set(emptyTp);
+ for (CharacterStyle span : styleSpans) {
+ span.updateDrawState(tp);
+ }
+ int tpUnderlineColor = 0;
+ float tpUnderlineThickness = 0.0f;
+
+ // These TextPaint fields only exist on Android ICS+ and are not in the SDK.
+ if (Versions.feature14Plus) {
+ tpUnderlineColor = (Integer)getField(tp, "underlineColor", 0);
+ tpUnderlineThickness = (Float)getField(tp, "underlineThickness", 0.0f);
+ }
+ if (tpUnderlineColor != 0) {
+ rangeStyles |= IME_RANGE_UNDERLINE | IME_RANGE_LINECOLOR;
+ rangeLineColor = tpUnderlineColor;
+ // Approximately translate underline thickness to what Gecko understands
+ if (tpUnderlineThickness <= 0.5f) {
+ rangeLineStyle = IME_RANGE_LINE_DOTTED;
+ } else {
+ rangeLineStyle = IME_RANGE_LINE_SOLID;
+ if (tpUnderlineThickness >= 2.0f) {
+ rangeBoldLine = true;
+ }
+ }
+ } else if (tp.isUnderlineText()) {
+ rangeStyles |= IME_RANGE_UNDERLINE;
+ rangeLineStyle = IME_RANGE_LINE_SOLID;
+ }
+ if (tp.getColor() != 0) {
+ rangeStyles |= IME_RANGE_FORECOLOR;
+ rangeForeColor = tp.getColor();
+ }
+ if (tp.bgColor != 0) {
+ rangeStyles |= IME_RANGE_BACKCOLOR;
+ rangeBackColor = tp.bgColor;
+ }
+ }
+ onImeAddCompositionRange(
+ rangeStart - composingStart, rangeEnd - composingStart,
+ rangeType, rangeStyles, rangeLineStyle, rangeBoldLine,
+ rangeForeColor, rangeBackColor, rangeLineColor);
+ rangeStart = rangeEnd;
+
+ if (DEBUG) {
+ Log.d(LOGTAG, " added " + rangeType +
+ " : " + Integer.toHexString(rangeStyles) +
+ " : " + Integer.toHexString(rangeForeColor) +
+ " : " + Integer.toHexString(rangeBackColor));
+ }
+ } while (rangeStart < composingEnd);
+ }
+
+ // GeckoEditableClient interface
+
+ @Override
+ public void sendKeyEvent(final KeyEvent event, int action, int metaState) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "sendKeyEvent(" + event + ", " + action + ", " + metaState + ")");
+ }
+ /*
+ We are actually sending two events to Gecko here,
+ 1. Event from the event parameter (key event)
+ 2. Sync event from the mActionQueue.offer call
+ The first event is a normal event that does not reply back to us,
+ the second sync event will have a reply, during which we see that there is a pending
+ event-type action, and update the selection/composition/etc. accordingly.
+ */
+ if (mNeedCompositionUpdate) {
+ // Make sure Gecko selection is in-sync with Java selection first.
+ icUpdateComposition();
+ }
+ onKeyEvent(event, action, metaState, /* isSynthesizedImeKey */ false);
+ mActionQueue.offer(new Action(Action.TYPE_EVENT));
+ }
+
+ @Override
+ public Editable getEditable() {
+ if (!onIcThread()) {
+ // Android may be holding an old InputConnection; ignore
+ if (DEBUG) {
+ Log.i(LOGTAG, "getEditable() called on non-IC thread");
+ }
+ return null;
+ }
+ if (mListener == null) {
+ // We haven't initialized or we've been destroyed.
+ return null;
+ }
+ return mProxy;
+ }
+
+ @Override
+ public void setBatchMode(boolean inBatchMode) {
+ if (!onIcThread()) {
+ // Android may be holding an old InputConnection; ignore
+ if (DEBUG) {
+ Log.i(LOGTAG, "setBatchMode() called on non-IC thread");
+ }
+ return;
+ }
+ if (!inBatchMode && mNeedCompositionUpdate) {
+ icUpdateComposition();
+ }
+ mInBatchMode = inBatchMode;
+ }
+
+ private void icUpdateComposition() {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ mNeedCompositionUpdate = false;
+ mActionQueue.syncWithGecko();
+ icMaybeSendComposition(mText, /* useEntireText */ false, /* notifyGecko */ true);
+ }
+
+ private void geckoScheduleCompositionUpdate() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ // May be called from either Gecko or IC thread
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mInBatchMode || !mNeedCompositionUpdate) {
+ return;
+ }
+ if (!mActionQueue.isEmpty()) {
+ // We are likely to block here, so wait a little and try again.
+ mIcPostHandler.postDelayed(this, 10);
+ return;
+ }
+ icUpdateComposition();
+ }
+ });
+ }
+
+ @Override
+ public void setSuppressKeyUp(boolean suppress) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ // Suppress key up event generated as a result of
+ // translating characters to key events
+ mSuppressKeyUp = suppress;
+ }
+
+ @Override
+ public Handler setInputConnectionHandler(Handler handler) {
+ if (handler == mIcPostHandler || !mFocused) {
+ return mIcPostHandler;
+ }
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ // There are three threads at this point: Gecko thread, old IC thread, and new IC
+ // thread, and we want to safely switch from old IC thread to new IC thread.
+ // We first send a TYPE_SET_HANDLER action to the Gecko thread; this ensures that
+ // the Gecko thread is stopped at a known point. At the same time, the old IC
+ // thread blocks on the action; this ensures that the old IC thread is stopped at
+ // a known point. Finally, inside the Gecko thread, we post a Runnable to the old
+ // IC thread; this Runnable switches from old IC thread to new IC thread. We
+ // switch IC thread on the old IC thread to ensure any pending Runnables on the
+ // old IC thread are processed before we switch over. Inside the Gecko thread, we
+ // also post a Runnable to the new IC thread; this Runnable blocks until the
+ // switch is complete; this ensures that the new IC thread won't accept
+ // InputConnection calls until after the switch.
+ mActionQueue.offer(Action.newSetHandler(handler));
+ mActionQueue.syncWithGecko();
+ return handler;
+ }
+
+ @Override // GeckoEditableClient
+ public void postToInputConnection(final Runnable runnable) {
+ mIcPostHandler.post(runnable);
+ }
+
+ private void geckoSetIcHandler(final Handler newHandler) {
+ geckoPostToIc(new Runnable() { // posting to old IC thread
+ @Override
+ public void run() {
+ synchronized (newHandler) {
+ mIcRunHandler = newHandler;
+ newHandler.notify();
+ }
+ }
+ });
+
+ // At this point, all future Runnables should be posted to the new IC thread, but
+ // we don't switch mIcRunHandler yet because there may be pending Runnables on the
+ // old IC thread still waiting to run.
+ mIcPostHandler = newHandler;
+
+ geckoPostToIc(new Runnable() { // posting to new IC thread
+ @Override
+ public void run() {
+ synchronized (newHandler) {
+ while (mIcRunHandler != newHandler) {
+ try {
+ newHandler.wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+ });
+ }
+
+ // GeckoEditableListener interface
+
+ private void geckoActionReply() {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ }
+
+ final Action action = mActionQueue.peek();
+ if (action == null) {
+ throw new IllegalStateException("empty actions queue");
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "reply: Action(" +
+ getConstantName(Action.class, "TYPE_", action.mType) + ")");
+ }
+ switch (action.mType) {
+ case Action.TYPE_SET_SPAN:
+ mText.setSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags);
+ break;
+
+ case Action.TYPE_REMOVE_SPAN:
+ mText.removeSpan(action.mSpanObject);
+ break;
+
+ case Action.TYPE_SET_HANDLER:
+ geckoSetIcHandler(action.mHandler);
+ break;
+ }
+
+ if (action.mUpdateComposition) {
+ geckoScheduleCompositionUpdate();
+ }
+ }
+
+ private void notifyCommitComposition() {
+ // Gecko already committed its composition. However, Android keyboards
+ // have trouble dealing with us removing the composition manually on
+ // the Java side. Therefore, we keep the composition intact on the Java
+ // side. The text content should still be in-sync on both sides.
+ }
+
+ private void notifyCancelComposition() {
+ // Composition should have been canceled on our side
+ // through text update notifications; verify that here.
+ if (DEBUG) {
+ final Object[] spans = mText.getSpans(0, mText.length(), Object.class);
+ for (Object span : spans) {
+ if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
+ throw new IllegalStateException("composition not cancelled");
+ }
+ }
+ }
+ }
+
+ @WrapForJNI @Override
+ public void notifyIME(final int type) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ // NOTIFY_IME_REPLY_EVENT is logged separately, inside geckoActionReply()
+ if (type != NOTIFY_IME_REPLY_EVENT) {
+ Log.d(LOGTAG, "notifyIME(" +
+ getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) +
+ ")");
+ }
+ }
+
+ if (type == NOTIFY_IME_REPLY_EVENT) {
+ try {
+ if (mGeckoFocused) {
+ // When mGeckoFocused is false, the reply is for a stale action,
+ // and we should not do anything
+ geckoActionReply();
+ } else if (DEBUG) {
+ Log.d(LOGTAG, "discarding stale reply");
+ }
+ } finally {
+ // Ensure action is always removed from queue
+ // even if stale action results in exception in geckoActionReply
+ mActionQueue.poll();
+ }
+ return;
+ } else if (type == NOTIFY_IME_TO_COMMIT_COMPOSITION) {
+ notifyCommitComposition();
+ return;
+ } else if (type == NOTIFY_IME_TO_CANCEL_COMPOSITION) {
+ notifyCancelComposition();
+ return;
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (type == NOTIFY_IME_OF_FOCUS) {
+ mFocused = true;
+ // Unmask events on the Gecko side
+ mActionQueue.offer(new Action(Action.TYPE_ACKNOWLEDGE_FOCUS));
+ }
+
+ // Make sure there are no other things going on. If we sent
+ // Action.TYPE_ACKNOWLEDGE_FOCUS, this line also makes us
+ // wait for Gecko to update us on the newly focused content
+ mActionQueue.syncWithGecko();
+ if (mListener != null) {
+ mListener.notifyIME(type);
+ }
+
+ // Unset mFocused after we call syncWithGecko because
+ // syncWithGecko becomes a no-op when mFocused is false.
+ if (type == NOTIFY_IME_OF_BLUR) {
+ mFocused = false;
+ }
+ }
+ });
+
+ // Register/unregister Gecko-side text selection listeners
+ // and update the mGeckoFocused flag.
+ if (type == NOTIFY_IME_OF_BLUR && mGeckoFocused) {
+ // Check for focus here because Gecko may send us a blur before a focus in some
+ // cases, and we don't want to unregister an event that was not registered.
+ mGeckoFocused = false;
+ mSuppressCompositions = false;
+ EventDispatcher.getInstance().
+ unregisterGeckoThreadListener(this, "TextSelection:DraggingHandle");
+ } else if (type == NOTIFY_IME_OF_FOCUS) {
+ mGeckoFocused = true;
+ mSuppressCompositions = false;
+ EventDispatcher.getInstance().
+ registerGeckoThreadListener(this, "TextSelection:DraggingHandle");
+ }
+ }
+
+ @WrapForJNI @Override
+ public void notifyIMEContext(final int state, final String typeHint,
+ final String modeHint, final String actionHint) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "notifyIMEContext(" +
+ getConstantName(GeckoEditableListener.class, "IME_STATE_", state) +
+ ", \"" + typeHint + "\", \"" + modeHint + "\", \"" + actionHint + "\")");
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.notifyIMEContext(state, typeHint, modeHint, actionHint);
+ }
+ });
+ }
+
+ @WrapForJNI @Override
+ public void onSelectionChange(int start, int end) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")");
+ }
+ if (start < 0 || start > mText.length() || end < 0 || end > mText.length()) {
+ Log.e(LOGTAG, "invalid selection notification range: " +
+ start + " to " + end + ", length: " + mText.length());
+ throw new IllegalArgumentException("invalid selection notification range");
+ }
+
+ if (mIgnoreSelectionChange) {
+ start = Selection.getSelectionStart(mText);
+ end = Selection.getSelectionEnd(mText);
+ mIgnoreSelectionChange = false;
+
+ } else {
+ Selection.setSelection(mText, start, end);
+ }
+
+ final int newStart = start;
+ final int newEnd = end;
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onSelectionChange(newStart, newEnd);
+ }
+ });
+ }
+
+ private void geckoReplaceText(int start, int oldEnd, CharSequence newText) {
+ mText.replace(start, oldEnd, newText);
+ }
+
+ private boolean geckoIsSameText(int start, int oldEnd, CharSequence newText) {
+ return oldEnd - start == newText.length() &&
+ TextUtils.regionMatches(mText, start, newText, 0, oldEnd - start);
+ }
+
+ @WrapForJNI @Override
+ public void onTextChange(final CharSequence text, final int start,
+ final int unboundedOldEnd, final int unboundedNewEnd) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ StringBuilder sb = new StringBuilder("onTextChange(");
+ debugAppend(sb, text);
+ sb.append(", ").append(start).append(", ")
+ .append(unboundedOldEnd).append(", ")
+ .append(unboundedNewEnd).append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (start < 0 || start > unboundedOldEnd) {
+ Log.e(LOGTAG, "invalid text notification range: " +
+ start + " to " + unboundedOldEnd);
+ throw new IllegalArgumentException("invalid text notification range");
+ }
+ /* For the "end" parameters, Gecko can pass in a large
+ number to denote "end of the text". Fix that here */
+ final int oldEnd = unboundedOldEnd > mText.length() ? mText.length() : unboundedOldEnd;
+ // new end should always match text
+ if (unboundedOldEnd <= mText.length() &&
+ unboundedNewEnd != (start + text.length())) {
+ Log.e(LOGTAG, "newEnd does not match text: " + unboundedNewEnd + " vs " +
+ (start + text.length()));
+ throw new IllegalArgumentException("newEnd does not match text");
+ }
+ final int newEnd = start + text.length();
+ final Action action = mActionQueue.peek();
+
+ if (action != null && action.mType == Action.TYPE_ACKNOWLEDGE_FOCUS) {
+ // Simply replace the text for newly-focused editors.
+ mText.replace(0, mText.length(), text);
+
+ } else if (action != null &&
+ action.mType == Action.TYPE_REPLACE_TEXT &&
+ start <= action.mStart &&
+ oldEnd >= action.mEnd &&
+ newEnd >= action.mStart + action.mSequence.length()) {
+
+ // Try to preserve both old spans and new spans in action.mSequence.
+ // indexInText is where we can find waction.mSequence within the passed in text.
+ final int startWithinText = action.mStart - start;
+ int indexInText = TextUtils.indexOf(text, action.mSequence, startWithinText);
+ if (indexInText < 0 && startWithinText >= action.mSequence.length()) {
+ indexInText = text.toString().lastIndexOf(action.mSequence.toString(),
+ startWithinText);
+ }
+
+ if (indexInText < 0) {
+ // Text was changed from under us. We are forced to discard any new spans.
+ geckoReplaceText(start, oldEnd, text);
+
+ // Don't ignore the next selection change because we are forced to re-sync
+ // with Gecko here.
+ mIgnoreSelectionChange = false;
+
+ } else if (indexInText == 0 && text.length() == action.mSequence.length()) {
+ // The new text exactly matches our sequence, so do a direct replace.
+ geckoReplaceText(start, oldEnd, action.mSequence);
+
+ // Ignore the next selection change because the selection change is a
+ // side-effect of the replace-text event we sent.
+ mIgnoreSelectionChange = true;
+
+ } else {
+ // The sequence is embedded within the changed text, so we have to perform
+ // replacement in parts. First replace part of text before the sequence.
+ geckoReplaceText(start, action.mStart, text.subSequence(0, indexInText));
+
+ // Then Replace the sequence itself to preserve new spans.
+ final int actionStart = indexInText + start;
+ geckoReplaceText(actionStart, actionStart + action.mEnd - action.mStart,
+ action.mSequence);
+
+ // Finally replace part of text after the sequence.
+ final int actionEnd = actionStart + action.mSequence.length();
+ geckoReplaceText(actionEnd, actionEnd + oldEnd - action.mEnd,
+ text.subSequence(actionEnd - start, text.length()));
+ }
+
+ } else if (geckoIsSameText(start, oldEnd, text)) {
+ // Nothing to do because the text is the same. This could happen when
+ // the composition is updated for example, in which case we want to keep the
+ // Java selection.
+ mIgnoreSelectionChange = mIgnoreSelectionChange ||
+ (action != null && action.mType == Action.TYPE_UPDATE_COMPOSITION);
+ return;
+
+ } else {
+ // Gecko side initiated the text change.
+ geckoReplaceText(start, oldEnd, text);
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onTextChange(text, start, oldEnd, newEnd);
+ }
+ });
+ }
+
+ @WrapForJNI @Override
+ public void onDefaultKeyEvent(final KeyEvent event) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ StringBuilder sb = new StringBuilder("onDefaultKeyEvent(");
+ sb.append("action=").append(event.getAction()).append(", ")
+ .append("keyCode=").append(event.getKeyCode()).append(", ")
+ .append("metaState=").append(event.getMetaState()).append(", ")
+ .append("time=").append(event.getEventTime()).append(", ")
+ .append("repeatCount=").append(event.getRepeatCount()).append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onDefaultKeyEvent(event);
+ }
+ });
+ }
+
+ // InvocationHandler interface
+
+ static String getConstantName(Class<?> cls, String prefix, Object value) {
+ for (Field fld : cls.getDeclaredFields()) {
+ try {
+ if (fld.getName().startsWith(prefix) &&
+ fld.get(null).equals(value)) {
+ return fld.getName();
+ }
+ } catch (IllegalAccessException e) {
+ }
+ }
+ return String.valueOf(value);
+ }
+
+ static StringBuilder debugAppend(StringBuilder sb, Object obj) {
+ if (obj == null) {
+ sb.append("null");
+ } else if (obj instanceof GeckoEditable) {
+ sb.append("GeckoEditable");
+ } else if (Proxy.isProxyClass(obj.getClass())) {
+ debugAppend(sb, Proxy.getInvocationHandler(obj));
+ } else if (obj instanceof CharSequence) {
+ sb.append('"').append(obj.toString().replace('\n', '\u21b2')).append('"');
+ } else if (obj.getClass().isArray()) {
+ sb.append(obj.getClass().getComponentType().getSimpleName()).append('[')
+ .append(Array.getLength(obj)).append(']');
+ } else {
+ sb.append(obj);
+ }
+ return sb;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ Object target;
+ final Class<?> methodInterface = method.getDeclaringClass();
+ if (DEBUG) {
+ // Editable methods should all be called from the IC thread
+ assertOnIcThread();
+ }
+ if (methodInterface == Editable.class ||
+ methodInterface == Appendable.class ||
+ methodInterface == Spannable.class) {
+ // Method alters the Editable; route calls to our implementation
+ target = this;
+ } else {
+ // Method queries the Editable; must sync with Gecko first
+ // then call on the inner Editable itself
+ mActionQueue.syncWithGecko();
+ target = mText;
+ }
+ Object ret;
+ try {
+ ret = method.invoke(target, args);
+ } catch (InvocationTargetException e) {
+ // Bug 817386
+ // Most likely Gecko has changed the text while GeckoInputConnection is
+ // trying to access the text. If we pass through the exception here, Fennec
+ // will crash due to a lack of exception handler. Log the exception and
+ // return an empty value instead.
+ if (!(e.getCause() instanceof IndexOutOfBoundsException)) {
+ // Only handle IndexOutOfBoundsException for now,
+ // as other exceptions might signal other bugs
+ throw e;
+ }
+ Log.w(LOGTAG, "Exception in GeckoEditable." + method.getName(), e.getCause());
+ Class<?> retClass = method.getReturnType();
+ if (retClass == Character.TYPE) {
+ ret = '\0';
+ } else if (retClass == Integer.TYPE) {
+ ret = 0;
+ } else if (retClass == String.class) {
+ ret = "";
+ } else {
+ ret = null;
+ }
+ }
+ if (DEBUG) {
+ StringBuilder log = new StringBuilder(method.getName());
+ log.append("(");
+ if (args != null) {
+ for (Object arg : args) {
+ debugAppend(log, arg).append(", ");
+ }
+ if (args.length > 0) {
+ log.setLength(log.length() - 2);
+ }
+ }
+ if (method.getReturnType().equals(Void.TYPE)) {
+ log.append(")");
+ } else {
+ debugAppend(log.append(") = "), ret);
+ }
+ Log.d(LOGTAG, log.toString());
+ }
+ return ret;
+ }
+
+ // Spannable interface
+
+ @Override
+ public void removeSpan(Object what) {
+ if (what == Selection.SELECTION_START ||
+ what == Selection.SELECTION_END) {
+ Log.w(LOGTAG, "selection removed with removeSpan()");
+ }
+
+ // "what" could be a composing span (BaseInputConnection.COMPOSING extends
+ // NoCopySpan), so we should update the Gecko composition
+ final boolean update = !mNeedCompositionUpdate && what instanceof NoCopySpan;
+ mActionQueue.offer(Action.newRemoveSpan(what, update));
+ mNeedCompositionUpdate |= update;
+ }
+
+ @Override
+ public void setSpan(Object what, int start, int end, int flags) {
+ // If mUpdateComposition is true, it means something in the queue will be
+ // scheduling an update, so it would be redundant to have this action schedule
+ // another update.
+ final boolean update = !mNeedCompositionUpdate &&
+ (flags & Spanned.SPAN_INTERMEDIATE) == 0 && (
+ (flags & Spanned.SPAN_COMPOSING) != 0 ||
+ what == Selection.SELECTION_START ||
+ what == Selection.SELECTION_END);
+ mActionQueue.offer(Action.newSetSpan(what, start, end, flags, update));
+ mNeedCompositionUpdate |= update;
+ }
+
+ // Appendable interface
+
+ @Override
+ public Editable append(CharSequence text) {
+ return replace(mProxy.length(), mProxy.length(), text, 0, text.length());
+ }
+
+ @Override
+ public Editable append(CharSequence text, int start, int end) {
+ return replace(mProxy.length(), mProxy.length(), text, start, end);
+ }
+
+ @Override
+ public Editable append(char text) {
+ return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1);
+ }
+
+ // Editable interface
+
+ @Override
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ @Override
+ public void setFilters(InputFilter[] filters) {
+ mFilters = filters;
+ }
+
+ @Override
+ public void clearSpans() {
+ /* XXX this clears the selection spans too,
+ but there is no way to clear the corresponding selection in Gecko */
+ Log.w(LOGTAG, "selection cleared with clearSpans()");
+ mText.clearSpans();
+ }
+
+ @Override
+ public Editable replace(int st, int en,
+ CharSequence source, int start, int end) {
+
+ CharSequence text = source;
+ if (start < 0 || start > end || end > text.length()) {
+ Log.e(LOGTAG, "invalid replace offsets: " +
+ start + " to " + end + ", length: " + text.length());
+ throw new IllegalArgumentException("invalid replace offsets");
+ }
+ if (start != 0 || end != text.length()) {
+ text = text.subSequence(start, end);
+ }
+ if (mFilters != null) {
+ // Filter text before sending the request to Gecko
+ for (int i = 0; i < mFilters.length; ++i) {
+ final CharSequence cs = mFilters[i].filter(
+ text, 0, text.length(), mProxy, st, en);
+ if (cs != null) {
+ text = cs;
+ }
+ }
+ }
+ if (text == source) {
+ // Always create a copy
+ text = new SpannableString(source);
+ }
+ mActionQueue.offer(Action.newReplaceText(text,
+ Math.min(st, en), Math.max(st, en)));
+ return mProxy;
+ }
+
+ @Override
+ public void clear() {
+ replace(0, mProxy.length(), "", 0, 0);
+ }
+
+ @Override
+ public Editable delete(int st, int en) {
+ return replace(st, en, "", 0, 0);
+ }
+
+ @Override
+ public Editable insert(int where, CharSequence text,
+ int start, int end) {
+ return replace(where, where, text, start, end);
+ }
+
+ @Override
+ public Editable insert(int where, CharSequence text) {
+ return replace(where, where, text, 0, text.length());
+ }
+
+ @Override
+ public Editable replace(int st, int en, CharSequence text) {
+ return replace(st, en, text, 0, text.length());
+ }
+
+ /* GetChars interface */
+
+ @Override
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ /* overridden Editable interface methods in GeckoEditable must not be called directly
+ outside of GeckoEditable. Instead, the call must go through mProxy, which ensures
+ that Java is properly synchronized with Gecko */
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ /* Spanned interface */
+
+ @Override
+ public int getSpanEnd(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int getSpanFlags(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int getSpanStart(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes") // nextSpanTransition uses raw Class in its Android declaration
+ public int nextSpanTransition(int start, int limit, Class type) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ /* CharSequence interface */
+
+ @Override
+ public char charAt(int index) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int length() {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ // GeckoEventListener implementation
+
+ @Override
+ public void handleMessage(String event, JSONObject message) {
+ if (!"TextSelection:DraggingHandle".equals(event)) {
+ return;
+ }
+
+ mSuppressCompositions = message.optBoolean("dragging", false);
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java
@@ -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 android.os.Handler;
+import android.text.Editable;
+import android.view.KeyEvent;
+
+/**
+ * Interface for the IC thread.
+ */
+interface GeckoEditableClient {
+ void sendKeyEvent(KeyEvent event, int action, int metaState);
+ Editable getEditable();
+ void setBatchMode(boolean isBatchMode);
+ void setSuppressKeyUp(boolean suppress);
+ Handler setInputConnectionHandler(Handler handler);
+ void postToInputConnection(Runnable runnable);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java
@@ -0,0 +1,41 @@
+/* -*- 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 android.view.KeyEvent;
+
+/**
+ * Interface for the Editable to listen on the Gecko thread, as well as for the IC thread to listen
+ * to the Editable.
+ */
+interface GeckoEditableListener {
+ // IME notification type for notifyIME(), corresponding to NotificationToIME enum in Gecko
+ @WrapForJNI
+ int NOTIFY_IME_OPEN_VKB = -2;
+ @WrapForJNI
+ int NOTIFY_IME_REPLY_EVENT = -1;
+ @WrapForJNI
+ int NOTIFY_IME_OF_FOCUS = 1;
+ @WrapForJNI
+ int NOTIFY_IME_OF_BLUR = 2;
+ @WrapForJNI
+ int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8;
+ @WrapForJNI
+ int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
+ // IME enabled state for notifyIMEContext()
+ int IME_STATE_DISABLED = 0;
+ int IME_STATE_ENABLED = 1;
+ int IME_STATE_PASSWORD = 2;
+ int IME_STATE_PLUGIN = 3;
+
+ void notifyIME(int type);
+ void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint);
+ void onSelectionChange(int start, int end);
+ void onTextChange(CharSequence text, int start, int oldEnd, int newEnd);
+ void onDefaultKeyEvent(KeyEvent event);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEvent.java
@@ -0,0 +1,634 @@
+/* -*- 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.nio.ByteBuffer;
+import java.util.concurrent.ArrayBlockingQueue;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.DisplayPortMetrics;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorManager;
+import android.location.Address;
+import android.location.Location;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+/**
+ * We're not allowed to hold on to most events given to us
+ * so we save the parts of the events we want to use in GeckoEvent.
+ * Fields have different meanings depending on the event type.
+ */
+@JNITarget
+public class GeckoEvent {
+ private static final String LOGTAG = "GeckoEvent";
+
+ private static final int EVENT_FACTORY_SIZE = 5;
+
+ // Maybe we're probably better to just make mType non final, and just store GeckoEvents in here...
+ private static final SparseArray<ArrayBlockingQueue<GeckoEvent>> mEvents = new SparseArray<ArrayBlockingQueue<GeckoEvent>>();
+
+ public static GeckoEvent get(NativeGeckoEvent type) {
+ synchronized (mEvents) {
+ ArrayBlockingQueue<GeckoEvent> events = mEvents.get(type.value);
+ if (events != null && events.size() > 0) {
+ return events.poll();
+ }
+ }
+
+ return new GeckoEvent(type);
+ }
+
+ public void recycle() {
+ synchronized (mEvents) {
+ ArrayBlockingQueue<GeckoEvent> events = mEvents.get(mType);
+ if (events == null) {
+ events = new ArrayBlockingQueue<GeckoEvent>(EVENT_FACTORY_SIZE);
+ mEvents.put(mType, events);
+ }
+
+ events.offer(this);
+ }
+ }
+
+ // Make sure to keep these values in sync with the enum in
+ // AndroidGeckoEvent in widget/android/AndroidJavaWrappers.h
+ @JNITarget
+ public enum NativeGeckoEvent {
+ NATIVE_POKE(0),
+ MOTION_EVENT(2),
+ SENSOR_EVENT(3),
+ LOCATION_EVENT(5),
+ LOAD_URI(12),
+ NOOP(15),
+ VIEWPORT(20),
+ VISITED(21),
+ NETWORK_CHANGED(22),
+ THUMBNAIL(25),
+ SCREENORIENTATION_CHANGED(27),
+ NATIVE_GESTURE_EVENT(31),
+ CALL_OBSERVER(33),
+ REMOVE_OBSERVER(34),
+ LOW_MEMORY(35),
+ NETWORK_LINK_CHANGE(36),
+ TELEMETRY_HISTOGRAM_ADD(37),
+ TELEMETRY_UI_SESSION_START(42),
+ TELEMETRY_UI_SESSION_STOP(43),
+ TELEMETRY_UI_EVENT(44),
+ GAMEPAD_ADDREMOVE(45),
+ GAMEPAD_DATA(46),
+ LONG_PRESS(47),
+ ZOOMEDVIEW(48);
+
+ public final int value;
+
+ private NativeGeckoEvent(int value) {
+ this.value = value;
+ }
+ }
+
+ public static final int ACTION_MAGNIFY_START = 11;
+ public static final int ACTION_MAGNIFY = 12;
+ public static final int ACTION_MAGNIFY_END = 13;
+
+ public static final int ACTION_GAMEPAD_ADDED = 1;
+ public static final int ACTION_GAMEPAD_REMOVED = 2;
+
+ public static final int ACTION_GAMEPAD_BUTTON = 1;
+ public static final int ACTION_GAMEPAD_AXES = 2;
+
+ private final int mType;
+ private int mAction;
+ private long mTime;
+ private Point[] mPoints;
+ private int[] mPointIndicies;
+ private int mPointerIndex; // index of the point that has changed
+ private float[] mOrientations;
+ private float[] mPressures;
+ private int[] mToolTypes;
+ private Point[] mPointRadii;
+ private Rect mRect;
+ private double mX;
+ private double mY;
+ private double mZ;
+ private double mW;
+
+ private int mMetaState;
+ private int mFlags;
+ private int mCount;
+ private String mCharacters;
+ private String mCharactersExtra;
+ private String mData;
+ private Location mLocation;
+
+ private int mConnectionType;
+ private boolean mIsWifi;
+ private int mDHCPGateway;
+
+ private short mScreenOrientation;
+ private short mScreenAngle;
+
+ private ByteBuffer mBuffer;
+
+ private int mWidth;
+ private int mHeight;
+
+ private int mID;
+ private int mGamepadButton;
+ private boolean mGamepadButtonPressed;
+ private float mGamepadButtonValue;
+ private float[] mGamepadValues;
+
+ private GeckoEvent(NativeGeckoEvent event) {
+ mType = event.value;
+ }
+
+ public static GeckoEvent createNoOpEvent() {
+ return GeckoEvent.get(NativeGeckoEvent.NOOP);
+ }
+
+ /**
+ * This method is a replacement for the the KeyEvent.isGamepadButton method to be
+ * compatible with Build.VERSION.SDK_INT < 12. This is an implementation of the
+ * same method isGamepadButton available after SDK 12.
+ * @param keyCode int with the key code (Android key constant from KeyEvent).
+ * @return True if the keycode is a gamepad button, such as {@link #KEYCODE_BUTTON_A}.
+ */
+ private static boolean isGamepadButton(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BUTTON_A:
+ case KeyEvent.KEYCODE_BUTTON_B:
+ case KeyEvent.KEYCODE_BUTTON_C:
+ case KeyEvent.KEYCODE_BUTTON_X:
+ case KeyEvent.KEYCODE_BUTTON_Y:
+ case KeyEvent.KEYCODE_BUTTON_Z:
+ case KeyEvent.KEYCODE_BUTTON_L1:
+ case KeyEvent.KEYCODE_BUTTON_R1:
+ case KeyEvent.KEYCODE_BUTTON_L2:
+ case KeyEvent.KEYCODE_BUTTON_R2:
+ case KeyEvent.KEYCODE_BUTTON_THUMBL:
+ case KeyEvent.KEYCODE_BUTTON_THUMBR:
+ case KeyEvent.KEYCODE_BUTTON_START:
+ case KeyEvent.KEYCODE_BUTTON_SELECT:
+ case KeyEvent.KEYCODE_BUTTON_MODE:
+ case KeyEvent.KEYCODE_BUTTON_1:
+ case KeyEvent.KEYCODE_BUTTON_2:
+ case KeyEvent.KEYCODE_BUTTON_3:
+ case KeyEvent.KEYCODE_BUTTON_4:
+ case KeyEvent.KEYCODE_BUTTON_5:
+ case KeyEvent.KEYCODE_BUTTON_6:
+ case KeyEvent.KEYCODE_BUTTON_7:
+ case KeyEvent.KEYCODE_BUTTON_8:
+ case KeyEvent.KEYCODE_BUTTON_9:
+ case KeyEvent.KEYCODE_BUTTON_10:
+ case KeyEvent.KEYCODE_BUTTON_11:
+ case KeyEvent.KEYCODE_BUTTON_12:
+ case KeyEvent.KEYCODE_BUTTON_13:
+ case KeyEvent.KEYCODE_BUTTON_14:
+ case KeyEvent.KEYCODE_BUTTON_15:
+ case KeyEvent.KEYCODE_BUTTON_16:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public static GeckoEvent createNativeGestureEvent(int action, PointF pt, double size) {
+ try {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NATIVE_GESTURE_EVENT);
+ event.mAction = action;
+ event.mCount = 1;
+ event.mPoints = new Point[1];
+
+ PointF geckoPoint = new PointF(pt.x, pt.y);
+ geckoPoint = GeckoAppShell.getLayerView().convertViewPointToLayerPoint(geckoPoint);
+
+ if (geckoPoint == null) {
+ // This could happen if Gecko isn't ready yet.
+ return null;
+ }
+
+ event.mPoints[0] = new Point((int)Math.floor(geckoPoint.x), (int)Math.floor(geckoPoint.y));
+
+ event.mX = size;
+ event.mTime = System.currentTimeMillis();
+ return event;
+ } catch (Exception e) {
+ // This can happen if Gecko isn't ready yet
+ return null;
+ }
+ }
+
+ /**
+ * Creates a GeckoEvent that contains the data from the MotionEvent.
+ * The keepInViewCoordinates parameter can be set to false to convert from the Java
+ * coordinate system (device pixels relative to the LayerView) to a coordinate system
+ * relative to gecko's coordinate system (CSS pixels relative to gecko scroll position).
+ */
+ public static GeckoEvent createMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.MOTION_EVENT);
+ event.initMotionEvent(m, keepInViewCoordinates);
+ return event;
+ }
+
+ /**
+ * Creates a GeckoEvent that contains the data from the LongPressEvent, to be
+ * dispatched in CSS pixels relative to gecko's scroll position.
+ */
+ public static GeckoEvent createLongPressEvent(MotionEvent m) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LONG_PRESS);
+ event.initMotionEvent(m, false);
+ return event;
+ }
+
+ private void initMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
+ mAction = m.getActionMasked();
+ mTime = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + m.getEventTime();
+ mMetaState = m.getMetaState();
+
+ switch (mAction) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_MOVE:
+ case MotionEvent.ACTION_HOVER_EXIT: {
+ mCount = m.getPointerCount();
+ mPoints = new Point[mCount];
+ mPointIndicies = new int[mCount];
+ mOrientations = new float[mCount];
+ mPressures = new float[mCount];
+ mToolTypes = new int[mCount];
+ mPointRadii = new Point[mCount];
+ mPointerIndex = m.getActionIndex();
+ for (int i = 0; i < mCount; i++) {
+ addMotionPoint(i, i, m, keepInViewCoordinates);
+ }
+ break;
+ }
+ default: {
+ mCount = 0;
+ mPointerIndex = -1;
+ mPoints = new Point[mCount];
+ mPointIndicies = new int[mCount];
+ mOrientations = new float[mCount];
+ mPressures = new float[mCount];
+ mToolTypes = new int[mCount];
+ mPointRadii = new Point[mCount];
+ }
+ }
+ }
+
+ private void addMotionPoint(int index, int eventIndex, MotionEvent event, boolean keepInViewCoordinates) {
+ try {
+ PointF geckoPoint = new PointF(event.getX(eventIndex), event.getY(eventIndex));
+ if (!keepInViewCoordinates) {
+ geckoPoint = GeckoAppShell.getLayerView().convertViewPointToLayerPoint(geckoPoint);
+ }
+
+ mPoints[index] = new Point((int)Math.floor(geckoPoint.x), (int)Math.floor(geckoPoint.y));
+ mPointIndicies[index] = event.getPointerId(eventIndex);
+
+ double radians = event.getOrientation(eventIndex);
+ mOrientations[index] = (float) Math.toDegrees(radians);
+ // w3c touchevents spec does not allow orientations == 90
+ // this shifts it to -90, which will be shifted to zero below
+ if (mOrientations[index] == 90)
+ mOrientations[index] = -90;
+
+ // w3c touchevent radius are given by an orientation between 0 and 90
+ // the radius is found by removing the orientation and measuring the x and y
+ // radius of the resulting ellipse
+ // for android orientations >= 0 and < 90, the major axis should correspond to
+ // just reporting the y radius as the major one, and x as minor
+ // however, for a radius < 0, we have to shift the orientation by adding 90, and
+ // reverse which radius is major and minor
+ if (mOrientations[index] < 0) {
+ mOrientations[index] += 90;
+ mPointRadii[index] = new Point((int) event.getToolMajor(eventIndex) / 2,
+ (int) event.getToolMinor(eventIndex) / 2);
+ } else {
+ mPointRadii[index] = new Point((int) event.getToolMinor(eventIndex) / 2,
+ (int) event.getToolMajor(eventIndex) / 2);
+ }
+
+ if (!keepInViewCoordinates) {
+ // If we are converting to gecko CSS pixels, then we should adjust the
+ // radii as well
+ float zoom = GeckoAppShell.getLayerView().getViewportMetrics().zoomFactor;
+ mPointRadii[index].x /= zoom;
+ mPointRadii[index].y /= zoom;
+ }
+ mPressures[index] = event.getPressure(eventIndex);
+ if (Versions.feature14Plus) {
+ mToolTypes[index] = event.getToolType(index);
+ }
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "Error creating motion point " + index, ex);
+ mPointRadii[index] = new Point(0, 0);
+ mPoints[index] = new Point(0, 0);
+ }
+ }
+
+ private static int HalSensorAccuracyFor(int androidAccuracy) {
+ switch (androidAccuracy) {
+ case SensorManager.SENSOR_STATUS_UNRELIABLE:
+ return GeckoHalDefines.SENSOR_ACCURACY_UNRELIABLE;
+ case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
+ return GeckoHalDefines.SENSOR_ACCURACY_LOW;
+ case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
+ return GeckoHalDefines.SENSOR_ACCURACY_MED;
+ case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
+ return GeckoHalDefines.SENSOR_ACCURACY_HIGH;
+ }
+ return GeckoHalDefines.SENSOR_ACCURACY_UNKNOWN;
+ }
+
+ public static GeckoEvent createSensorEvent(SensorEvent s) {
+ int sensor_type = s.sensor.getType();
+ GeckoEvent event = null;
+
+ switch (sensor_type) {
+
+ case Sensor.TYPE_ACCELEROMETER:
+ event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+ event.mFlags = GeckoHalDefines.SENSOR_ACCELERATION;
+ event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+ event.mX = s.values[0];
+ event.mY = s.values[1];
+ event.mZ = s.values[2];
+ break;
+
+ case Sensor.TYPE_LINEAR_ACCELERATION:
+ event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+ event.mFlags = GeckoHalDefines.SENSOR_LINEAR_ACCELERATION;
+ event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+ event.mX = s.values[0];
+ event.mY = s.values[1];
+ event.mZ = s.values[2];
+ break;
+
+ case Sensor.TYPE_ORIENTATION:
+ event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+ event.mFlags = GeckoHalDefines.SENSOR_ORIENTATION;
+ event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+ event.mX = s.values[0];
+ event.mY = s.values[1];
+ event.mZ = s.values[2];
+ break;
+
+ case Sensor.TYPE_GYROSCOPE:
+ event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+ event.mFlags = GeckoHalDefines.SENSOR_GYROSCOPE;
+ event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+ event.mX = Math.toDegrees(s.values[0]);
+ event.mY = Math.toDegrees(s.values[1]);
+ event.mZ = Math.toDegrees(s.values[2]);
+ break;
+
+ case Sensor.TYPE_PROXIMITY:
+ event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+ event.mFlags = GeckoHalDefines.SENSOR_PROXIMITY;
+ event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+ event.mX = s.values[0];
+ event.mY = 0;
+ event.mZ = s.sensor.getMaximumRange();
+ break;
+
+ case Sensor.TYPE_LIGHT:
+ event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+ event.mFlags = GeckoHalDefines.SENSOR_LIGHT;
+ event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+ event.mX = s.values[0];
+ break;
+
+ case Sensor.TYPE_ROTATION_VECTOR:
+ case Sensor.TYPE_GAME_ROTATION_VECTOR: // API >= 18
+ event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
+ event.mFlags = (sensor_type == Sensor.TYPE_ROTATION_VECTOR ?
+ GeckoHalDefines.SENSOR_ROTATION_VECTOR :
+ GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR);
+ event.mMetaState = HalSensorAccuracyFor(s.accuracy);
+ event.mX = s.values[0];
+ event.mY = s.values[1];
+ event.mZ = s.values[2];
+ if (s.values.length >= 4) {
+ event.mW = s.values[3];
+ } else {
+ // s.values[3] was optional in API <= 18, so we need to compute it
+ // The values form a unit quaternion, so we can compute the angle of
+ // rotation purely based on the given 3 values.
+ event.mW = 1 - s.values[0] * s.values[0] - s.values[1] * s.values[1] - s.values[2] * s.values[2];
+ event.mW = (event.mW > 0.0) ? Math.sqrt(event.mW) : 0.0;
+ }
+ break;
+ }
+
+ // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds.
+ event.mTime = s.timestamp / 1000;
+ return event;
+ }
+
+ public static GeckoEvent createLocationEvent(Location l) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOCATION_EVENT);
+ event.mLocation = l;
+ return event;
+ }
+
+ public static GeckoEvent createViewportEvent(ImmutableViewportMetrics metrics, DisplayPortMetrics displayPort) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.VIEWPORT);
+ event.mCharacters = "Viewport:Change";
+ StringBuilder sb = new StringBuilder(256);
+ sb.append("{ \"x\" : ").append(metrics.viewportRectLeft)
+ .append(", \"y\" : ").append(metrics.viewportRectTop)
+ .append(", \"zoom\" : ").append(metrics.zoomFactor)
+ .append(", \"displayPort\" :").append(displayPort.toJSON())
+ .append('}');
+ event.mCharactersExtra = sb.toString();
+ return event;
+ }
+
+ public static GeckoEvent createURILoadEvent(String uri) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOAD_URI);
+ event.mCharacters = uri;
+ event.mCharactersExtra = "";
+ return event;
+ }
+
+ public static GeckoEvent createBookmarkLoadEvent(String uri) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOAD_URI);
+ event.mCharacters = uri;
+ event.mCharactersExtra = "-bookmark";
+ return event;
+ }
+
+ public static GeckoEvent createVisitedEvent(String data) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.VISITED);
+ event.mCharacters = data;
+ return event;
+ }
+
+ public static GeckoEvent createNetworkEvent(int connectionType, boolean isWifi, int DHCPGateway, String status) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NETWORK_CHANGED);
+ event.mConnectionType = connectionType;
+ event.mIsWifi = isWifi;
+ event.mDHCPGateway = DHCPGateway;
+ event.mCharacters = status;
+ return event;
+ }
+
+ public static GeckoEvent createThumbnailEvent(int tabId, int bufw, int bufh, ByteBuffer buffer) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.THUMBNAIL);
+ event.mPoints = new Point[1];
+ event.mPoints[0] = new Point(bufw, bufh);
+ event.mMetaState = tabId;
+ event.mBuffer = buffer;
+ return event;
+ }
+
+ public static GeckoEvent createZoomedViewEvent(int tabId, int x, int y, int bufw, int bufh, float scaleFactor, ByteBuffer buffer) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.ZOOMEDVIEW);
+ event.mPoints = new Point[2];
+ event.mPoints[0] = new Point(x, y);
+ event.mPoints[1] = new Point(bufw, bufh);
+ event.mX = (double) scaleFactor;
+ event.mMetaState = tabId;
+ event.mBuffer = buffer;
+ return event;
+ }
+
+ public static GeckoEvent createScreenOrientationEvent(short aScreenOrientation, short aScreenAngle) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.SCREENORIENTATION_CHANGED);
+ event.mScreenOrientation = aScreenOrientation;
+ event.mScreenAngle = aScreenAngle;
+ return event;
+ }
+
+ public static GeckoEvent createCallObserverEvent(String observerKey, String topic, String data) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.CALL_OBSERVER);
+ event.mCharacters = observerKey;
+ event.mCharactersExtra = topic;
+ event.mData = data;
+ return event;
+ }
+
+ public static GeckoEvent createRemoveObserverEvent(String observerKey) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.REMOVE_OBSERVER);
+ event.mCharacters = observerKey;
+ return event;
+ }
+
+ public static GeckoEvent createLowMemoryEvent(int level) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LOW_MEMORY);
+ event.mMetaState = level;
+ return event;
+ }
+
+ public static GeckoEvent createNetworkLinkChangeEvent(String status) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.NETWORK_LINK_CHANGE);
+ event.mCharacters = status;
+ return event;
+ }
+
+ public static GeckoEvent createTelemetryHistogramAddEvent(String histogram,
+ int value) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
+ event.mCharacters = histogram;
+ // Set the extras with null so that it cannot be mistaken with a keyed histogram.
+ event.mCharactersExtra = null;
+ event.mCount = value;
+ return event;
+ }
+
+ public static GeckoEvent createTelemetryKeyedHistogramAddEvent(String histogram,
+ String key,
+ int value) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
+ event.mCharacters = histogram;
+ event.mCharactersExtra = key;
+ event.mCount = value;
+ return event;
+ }
+
+ public static GeckoEvent createTelemetryUISessionStartEvent(String session, long timestamp) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_SESSION_START);
+ event.mCharacters = session;
+ event.mTime = timestamp;
+ return event;
+ }
+
+ public static GeckoEvent createTelemetryUISessionStopEvent(String session, String reason, long timestamp) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_SESSION_STOP);
+ event.mCharacters = session;
+ event.mCharactersExtra = reason;
+ event.mTime = timestamp;
+ return event;
+ }
+
+ public static GeckoEvent createTelemetryUIEvent(String action, String method, long timestamp, String extras) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_EVENT);
+ event.mData = action;
+ event.mCharacters = method;
+ event.mCharactersExtra = extras;
+ event.mTime = timestamp;
+ return event;
+ }
+
+ public static GeckoEvent createGamepadAddRemoveEvent(int id, boolean added) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_ADDREMOVE);
+ event.mID = id;
+ event.mAction = added ? ACTION_GAMEPAD_ADDED : ACTION_GAMEPAD_REMOVED;
+ return event;
+ }
+
+ private static int boolArrayToBitfield(boolean[] array) {
+ int bits = 0;
+ for (int i = 0; i < array.length; i++) {
+ if (array[i]) {
+ bits |= 1 << i;
+ }
+ }
+ return bits;
+ }
+
+ public static GeckoEvent createGamepadButtonEvent(int id,
+ int which,
+ boolean pressed,
+ float value) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
+ event.mID = id;
+ event.mAction = ACTION_GAMEPAD_BUTTON;
+ event.mGamepadButton = which;
+ event.mGamepadButtonPressed = pressed;
+ event.mGamepadButtonValue = value;
+ return event;
+ }
+
+ public static GeckoEvent createGamepadAxisEvent(int id, boolean[] valid,
+ float[] values) {
+ GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
+ event.mID = id;
+ event.mAction = ACTION_GAMEPAD_AXES;
+ event.mFlags = boolArrayToBitfield(valid);
+ event.mCount = values.length;
+ event.mGamepadValues = values;
+ return event;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java
@@ -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;
+
+public class GeckoHalDefines
+{
+ /*
+ * Keep these values consistent with |SensorType| in HalSensor.h
+ */
+ public static final int SENSOR_ORIENTATION = 0;
+ public static final int SENSOR_ACCELERATION = 1;
+ public static final int SENSOR_PROXIMITY = 2;
+ public static final int SENSOR_LINEAR_ACCELERATION = 3;
+ public static final int SENSOR_GYROSCOPE = 4;
+ public static final int SENSOR_LIGHT = 5;
+ public static final int SENSOR_ROTATION_VECTOR = 6;
+ public static final int SENSOR_GAME_ROTATION_VECTOR = 7;
+
+ public static final int SENSOR_ACCURACY_UNKNOWN = -1;
+ public static final int SENSOR_ACCURACY_UNRELIABLE = 0;
+ public static final int SENSOR_ACCURACY_LOW = 1;
+ public static final int SENSOR_ACCURACY_MED = 2;
+ public static final int SENSOR_ACCURACY_HIGH = 3;
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -0,0 +1,992 @@
+/* -*- 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.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.SynchronousQueue;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.util.Clipboard;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Selection;
+import android.text.SpannableString;
+import android.text.method.KeyListener;
+import android.text.method.TextKeyListener;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+class GeckoInputConnection
+ extends BaseInputConnection
+ implements InputConnectionListener, GeckoEditableListener {
+
+ private static final boolean DEBUG = false;
+ protected static final String LOGTAG = "GeckoInputConnection";
+
+ private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection";
+ private static final String CUSTOM_HANDLER_TEST_CLASS =
+ "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput";
+
+ private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480;
+
+ private static Handler sBackgroundHandler;
+
+ // Managed only by notifyIMEContext; see comments in notifyIMEContext
+ private int mIMEState;
+ private String mIMETypeHint = "";
+ private String mIMEModeHint = "";
+ private String mIMEActionHint = "";
+
+ private String mCurrentInputMethod = "";
+
+ private final View mView;
+ private final GeckoEditableClient mEditableClient;
+ protected int mBatchEditCount;
+ private ExtractedTextRequest mUpdateRequest;
+ private final ExtractedText mUpdateExtract = new ExtractedText();
+ private boolean mBatchSelectionChanged;
+ private boolean mBatchTextChanged;
+ private final InputConnection mKeyInputConnection;
+
+ public static GeckoEditableListener create(View targetView,
+ GeckoEditableClient editable) {
+ if (DEBUG)
+ return DebugGeckoInputConnection.create(targetView, editable);
+ else
+ return new GeckoInputConnection(targetView, editable);
+ }
+
+ protected GeckoInputConnection(View targetView,
+ GeckoEditableClient editable) {
+ super(targetView, true);
+ mView = targetView;
+ mEditableClient = editable;
+ mIMEState = IME_STATE_DISABLED;
+ // InputConnection that sends keys for plugins, which don't have full editors
+ mKeyInputConnection = new BaseInputConnection(targetView, false);
+ }
+
+ @Override
+ public synchronized boolean beginBatchEdit() {
+ mBatchEditCount++;
+ mEditableClient.setBatchMode(true);
+ return true;
+ }
+
+ @Override
+ public synchronized boolean endBatchEdit() {
+ if (mBatchEditCount > 0) {
+ mBatchEditCount--;
+ if (mBatchEditCount == 0) {
+ if (mBatchTextChanged) {
+ notifyTextChange();
+ mBatchTextChanged = false;
+ }
+ if (mBatchSelectionChanged) {
+ Editable editable = getEditable();
+ notifySelectionChange(Selection.getSelectionStart(editable),
+ Selection.getSelectionEnd(editable));
+ mBatchSelectionChanged = false;
+ }
+ mEditableClient.setBatchMode(false);
+ }
+ } else {
+ Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!");
+ }
+ return true;
+ }
+
+ @Override
+ public Editable getEditable() {
+ return mEditableClient.getEditable();
+ }
+
+ @Override
+ public boolean performContextMenuAction(int id) {
+ Editable editable = getEditable();
+ if (editable == null) {
+ return false;
+ }
+ int selStart = Selection.getSelectionStart(editable);
+ int selEnd = Selection.getSelectionEnd(editable);
+
+ switch (id) {
+ case android.R.id.selectAll:
+ setSelection(0, editable.length());
+ break;
+ case android.R.id.cut:
+ // If selection is empty, we'll select everything
+ if (selStart == selEnd) {
+ // Fill the clipboard
+ Clipboard.setText(editable);
+ editable.clear();
+ } else {
+ Clipboard.setText(
+ editable.toString().substring(
+ Math.min(selStart, selEnd),
+ Math.max(selStart, selEnd)));
+ editable.delete(selStart, selEnd);
+ }
+ break;
+ case android.R.id.paste:
+ commitText(Clipboard.getText(), 1);
+ break;
+ case android.R.id.copy:
+ // Copy the current selection or the empty string if nothing is selected.
+ String copiedText = selStart == selEnd ? "" :
+ editable.toString().substring(
+ Math.min(selStart, selEnd),
+ Math.max(selStart, selEnd));
+ Clipboard.setText(copiedText);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean performPrivateCommand(final String action, final Bundle data) {
+ switch (action) {
+ case "process-gecko-events":
+ // Process all currently pending Gecko thread events before returning.
+
+ final Editable editable = getEditable();
+ if (editable == null) {
+ return false;
+ }
+
+ // Removing an invalid span is essentially a no-op, but it does force the
+ // current thread to wait for the Gecko thread when we call length(), in order
+ // to process the removeSpan event. Once Gecko thread processes the removeSpan
+ // event, all previous events in the Gecko event queue would have been
+ // processed as well.
+ editable.removeSpan(null);
+ editable.length();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
+ if (req == null)
+ return null;
+
+ if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0)
+ mUpdateRequest = req;
+
+ Editable editable = getEditable();
+ if (editable == null) {
+ return null;
+ }
+ int selStart = Selection.getSelectionStart(editable);
+ int selEnd = Selection.getSelectionEnd(editable);
+
+ ExtractedText extract = new ExtractedText();
+ extract.flags = 0;
+ extract.partialStartOffset = -1;
+ extract.partialEndOffset = -1;
+ extract.selectionStart = selStart;
+ extract.selectionEnd = selEnd;
+ extract.startOffset = 0;
+ if ((req.flags & GET_TEXT_WITH_STYLES) != 0) {
+ extract.text = new SpannableString(editable);
+ } else {
+ extract.text = editable.toString();
+ }
+ return extract;
+ }
+
+ private View getView() {
+ return mView;
+ }
+
+ private InputMethodManager getInputMethodManager() {
+ View view = getView();
+ if (view == null) {
+ return null;
+ }
+ Context context = view.getContext();
+ return InputMethods.getInputMethodManager(context);
+ }
+
+ private void showSoftInput() {
+ final View v = getView();
+ final InputMethodManager imm = getInputMethodManager();
+ if (v == null || imm == null) {
+ return;
+ }
+
+ v.post(new Runnable() {
+ @Override
+ public void run() {
+ if (v.hasFocus() && !imm.isActive(v)) {
+ // Marshmallow workaround: The view has focus but it is not the active
+ // view for the input method. (Bug 1211848)
+ v.clearFocus();
+ v.requestFocus();
+ }
+ imm.showSoftInput(v, 0);
+ }
+ });
+ }
+
+ private void hideSoftInput() {
+ final InputMethodManager imm = getInputMethodManager();
+ if (imm != null) {
+ final View v = getView();
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ }
+ }
+
+ private void restartInput() {
+
+ final InputMethodManager imm = getInputMethodManager();
+ if (imm == null) {
+ return;
+ }
+ final View v = getView();
+ // InputMethodManager has internal logic to detect if we are restarting input
+ // in an already focused View, which is the case here because all content text
+ // fields are inside one LayerView. When this happens, InputMethodManager will
+ // tell the input method to soft reset instead of hard reset. Stock latin IME
+ // on Android 4.2+ has a quirk that when it soft resets, it does not clear the
+ // composition. The following workaround tricks the IME into clearing the
+ // composition when soft resetting.
+ if (InputMethods.needsSoftResetWorkaround(mCurrentInputMethod)) {
+ // Fake a selection change, because the IME clears the composition when
+ // the selection changes, even if soft-resetting. Offsets here must be
+ // different from the previous selection offsets, and -1 seems to be a
+ // reasonable, deterministic value
+ notifySelectionChange(-1, -1);
+ }
+ try {
+ imm.restartInput(v);
+ } catch (RuntimeException e) {
+ Log.e(LOGTAG, "Error restarting input", e);
+ }
+ }
+
+ private void resetInputConnection() {
+ if (mBatchEditCount != 0) {
+ Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
+ mBatchEditCount = 0;
+ }
+ mBatchSelectionChanged = false;
+ mBatchTextChanged = false;
+
+ // Do not reset mIMEState here; see comments in notifyIMEContext
+ }
+
+ @Override
+ public void onTextChange(CharSequence text, int start, int oldEnd, int newEnd) {
+
+ if (mUpdateRequest == null) {
+ // Android always expects selection updates when not in extracted mode;
+ // in extracted mode, the selection is reported through updateExtractedText
+ final Editable editable = getEditable();
+ if (editable != null) {
+ onSelectionChange(Selection.getSelectionStart(editable),
+ Selection.getSelectionEnd(editable));
+ }
+ return;
+ }
+
+ if (mBatchEditCount > 0) {
+ // Delay notification until after the batch edit
+ mBatchTextChanged = true;
+ return;
+ }
+ notifyTextChange();
+ }
+
+ private void notifyTextChange() {
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ final Editable editable = getEditable();
+ if (imm == null || v == null || editable == null) {
+ return;
+ }
+ mUpdateExtract.flags = 0;
+ // Update the entire Editable range
+ mUpdateExtract.partialStartOffset = -1;
+ mUpdateExtract.partialEndOffset = -1;
+ mUpdateExtract.selectionStart =
+ Selection.getSelectionStart(editable);
+ mUpdateExtract.selectionEnd =
+ Selection.getSelectionEnd(editable);
+ mUpdateExtract.startOffset = 0;
+ if ((mUpdateRequest.flags & GET_TEXT_WITH_STYLES) != 0) {
+ mUpdateExtract.text = new SpannableString(editable);
+ } else {
+ mUpdateExtract.text = editable.toString();
+ }
+ imm.updateExtractedText(v, mUpdateRequest.token,
+ mUpdateExtract);
+ }
+
+ @Override
+ public void onSelectionChange(int start, int end) {
+
+ if (mBatchEditCount > 0) {
+ // Delay notification until after the batch edit
+ mBatchSelectionChanged = true;
+ return;
+ }
+
+ final Editable editable = getEditable();
+ if (editable != null) {
+ notifySelectionChange(Selection.getSelectionStart(editable),
+ Selection.getSelectionEnd(editable));
+ }
+ }
+
+ private void notifySelectionChange(int start, int end) {
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ final Editable editable = getEditable();
+ if (imm == null || v == null || editable == null) {
+ return;
+ }
+ imm.updateSelection(v, start, end, getComposingSpanStart(editable),
+ getComposingSpanEnd(editable));
+ }
+
+ @Override
+ public void onDefaultKeyEvent(final KeyEvent event) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoInputConnection.this.performDefaultKeyAction(event);
+ }
+ });
+ }
+
+ private static synchronized Handler getBackgroundHandler() {
+ if (sBackgroundHandler != null) {
+ return sBackgroundHandler;
+ }
+ // Don't use GeckoBackgroundThread because Gecko thread may block waiting on
+ // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME,
+ // GeckoBackgroundThread may end up also block waiting on Gecko thread and a
+ // deadlock occurs
+ Thread backgroundThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ synchronized (GeckoInputConnection.class) {
+ sBackgroundHandler = new Handler();
+ GeckoInputConnection.class.notify();
+ }
+ Looper.loop();
+ // We should never be exiting the thread loop.
+ throw new IllegalThreadStateException("unreachable code");
+ }
+ }, LOGTAG);
+ backgroundThread.setDaemon(true);
+ backgroundThread.start();
+ while (sBackgroundHandler == null) {
+ try {
+ // wait for new thread to set sBackgroundHandler
+ GeckoInputConnection.class.wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ return sBackgroundHandler;
+ }
+
+ private boolean canReturnCustomHandler() {
+ if (mIMEState == IME_STATE_DISABLED) {
+ return false;
+ }
+ for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
+ // We only return our custom Handler to InputMethodManager's InputConnection
+ // proxy. For all other purposes, we return the regular Handler.
+ // InputMethodManager retrieves the Handler for its InputConnection proxy
+ // inside its method startInputInner(), so we check for that here. This is
+ // valid from Android 2.2 to at least Android 4.2. If this situation ever
+ // changes, we gracefully fall back to using the regular Handler.
+ if ("startInputInner".equals(frame.getMethodName()) &&
+ "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) {
+ // only return our own Handler to InputMethodManager
+ return true;
+ }
+ if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) &&
+ CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) {
+ // InputConnection tests should also run on the custom handler
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isPhysicalKeyboardPresent() {
+ final View v = getView();
+ if (v == null) {
+ return false;
+ }
+ final Configuration config = v.getContext().getResources().getConfiguration();
+ return config.keyboard != Configuration.KEYBOARD_NOKEYS;
+ }
+
+ // Android N: @Override // InputConnection
+ public Handler getHandler() {
+ if (isPhysicalKeyboardPresent()) {
+ return ThreadUtils.getUiHandler();
+ }
+
+ return getBackgroundHandler();
+ }
+
+ @Override // InputConnectionListener
+ public Handler getHandler(Handler defHandler) {
+ if (!canReturnCustomHandler()) {
+ return defHandler;
+ }
+
+ return mEditableClient.setInputConnectionHandler(getHandler());
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ // Some keyboards require us to fill out outAttrs even if we return null.
+ outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
+ outAttrs.actionLabel = null;
+
+ if (mIMEState == IME_STATE_DISABLED) {
+ hideSoftInput();
+ return null;
+ }
+
+ if (mIMEState == IME_STATE_PASSWORD ||
+ "password".equalsIgnoreCase(mIMETypeHint))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ else if (mIMEState == IME_STATE_PLUGIN)
+ outAttrs.inputType = InputType.TYPE_NULL; // "send key events" mode
+ else if (mIMETypeHint.equalsIgnoreCase("url"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
+ else if (mIMETypeHint.equalsIgnoreCase("email"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ else if (mIMETypeHint.equalsIgnoreCase("tel"))
+ outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
+ else if (mIMETypeHint.equalsIgnoreCase("number") ||
+ mIMETypeHint.equalsIgnoreCase("range"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
+ | InputType.TYPE_NUMBER_FLAG_SIGNED
+ | InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ else if (mIMETypeHint.equalsIgnoreCase("week") ||
+ mIMETypeHint.equalsIgnoreCase("month"))
+ outAttrs.inputType = InputType.TYPE_CLASS_DATETIME
+ | InputType.TYPE_DATETIME_VARIATION_DATE;
+ else if (mIMEModeHint.equalsIgnoreCase("numeric"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
+ InputType.TYPE_NUMBER_FLAG_SIGNED |
+ InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ else if (mIMEModeHint.equalsIgnoreCase("digit"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
+ else {
+ // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+ InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
+ if (mIMETypeHint.equalsIgnoreCase("textarea") ||
+ mIMETypeHint.length() == 0) {
+ // empty mIMETypeHint indicates contentEditable/designMode documents
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ if (mIMEModeHint.equalsIgnoreCase("uppercase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+ else if (mIMEModeHint.equalsIgnoreCase("titlecase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
+ else if (mIMETypeHint.equalsIgnoreCase("text") &&
+ !mIMEModeHint.equalsIgnoreCase("autocapitalized"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_NORMAL;
+ else if (!mIMEModeHint.equalsIgnoreCase("lowercase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ // auto-capitalized mode is the default for types other than text
+ }
+
+ if (mIMEActionHint.equalsIgnoreCase("go"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
+ else if (mIMEActionHint.equalsIgnoreCase("done"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
+ else if (mIMEActionHint.equalsIgnoreCase("next"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
+ else if (mIMEActionHint.equalsIgnoreCase("search") ||
+ mIMETypeHint.equalsIgnoreCase("search"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
+ else if (mIMEActionHint.equalsIgnoreCase("send"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
+ else if (mIMEActionHint.length() > 0) {
+ if (DEBUG)
+ Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\"");
+ outAttrs.actionLabel = mIMEActionHint;
+ }
+
+ Context context = getView().getContext();
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) {
+ // prevent showing full-screen keyboard only when the screen is tall enough
+ // to show some reasonable amount of the page (see bug 752709)
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI
+ | EditorInfo.IME_FLAG_NO_FULLSCREEN;
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "mapped IME states to: inputType = " +
+ Integer.toHexString(outAttrs.inputType) + ", imeOptions = " +
+ Integer.toHexString(outAttrs.imeOptions));
+ }
+
+ String prevInputMethod = mCurrentInputMethod;
+ mCurrentInputMethod = InputMethods.getCurrentInputMethod(context);
+ if (DEBUG) {
+ Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod);
+ }
+
+ // If the user has changed IMEs, then notify input method observers.
+ if (!mCurrentInputMethod.equals(prevInputMethod) && GeckoAppShell.getGeckoInterface() != null) {
+ GeckoAppShell.getGeckoInterface().onInputMethodChanged(mCurrentInputMethod);
+ }
+
+ if (mIMEState == IME_STATE_PLUGIN) {
+ // Since we are using a temporary string as the editable, the selection is at 0
+ outAttrs.initialSelStart = 0;
+ outAttrs.initialSelEnd = 0;
+ return mKeyInputConnection;
+ }
+ Editable editable = getEditable();
+ outAttrs.initialSelStart = Selection.getSelectionStart(editable);
+ outAttrs.initialSelEnd = Selection.getSelectionEnd(editable);
+
+ showSoftInput();
+ return this;
+ }
+
+ private boolean replaceComposingSpanWithSelection() {
+ final Editable content = getEditable();
+ if (content == null) {
+ return false;
+ }
+ int a = getComposingSpanStart(content),
+ b = getComposingSpanEnd(content);
+ if (a != -1 && b != -1) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "removing composition at " + a + "-" + b);
+ }
+ removeComposingSpans(content);
+ Selection.setSelection(content, a, b);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean commitText(CharSequence text, int newCursorPosition) {
+ if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod) &&
+ text.length() == 1 && newCursorPosition > 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "committing \"" + text + "\" as key");
+ }
+ // mKeyInputConnection is a BaseInputConnection that commits text as keys;
+ // but we first need to replace any composing span with a selection,
+ // so that the new key events will generate characters to replace
+ // text from the old composing span
+ return replaceComposingSpanWithSelection() &&
+ mKeyInputConnection.commitText(text, newCursorPosition);
+ }
+ return super.commitText(text, newCursorPosition);
+ }
+
+ @Override
+ public boolean setSelection(int start, int end) {
+ if (start < 0 || end < 0) {
+ // Some keyboards (e.g. Samsung) can call setSelection with
+ // negative offsets. In that case we ignore the call, similar to how
+ // BaseInputConnection.setSelection ignores offsets that go past the length.
+ return true;
+ }
+ return super.setSelection(start, end);
+ }
+
+ /* package */ void sendKeyEvent(final int action, KeyEvent event) {
+ final Editable editable = getEditable();
+ if (editable == null) {
+ return;
+ }
+
+ final KeyListener keyListener = TextKeyListener.getInstance();
+ event = translateKey(event.getKeyCode(), event);
+
+ // We only let TextKeyListener do UI things on the UI thread.
+ final View v = ThreadUtils.isOnUiThread() ? getView() : null;
+ final int keyCode = event.getKeyCode();
+ final boolean handled;
+
+ if (shouldSkipKeyListener(keyCode, event)) {
+ handled = false;
+ } else if (action == KeyEvent.ACTION_DOWN) {
+ mEditableClient.setSuppressKeyUp(true);
+ handled = keyListener.onKeyDown(v, editable, keyCode, event);
+ } else if (action == KeyEvent.ACTION_UP) {
+ handled = keyListener.onKeyUp(v, editable, keyCode, event);
+ } else {
+ handled = keyListener.onKeyOther(v, editable, event);
+ }
+
+ if (!handled) {
+ mEditableClient.sendKeyEvent(event, action, TextKeyListener.getMetaState(editable));
+ }
+
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (!handled) {
+ // Usually, the down key listener call above adjusts meta states for us.
+ // However, if the call didn't handle the event, we have to manually
+ // adjust meta states so the meta states remain consistent.
+ TextKeyListener.adjustMetaAfterKeypress(editable);
+ }
+ mEditableClient.setSuppressKeyUp(false);
+ }
+ }
+
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ sendKeyEvent(event.getAction(), event);
+ return false; // seems to always return false
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ private boolean shouldProcessKey(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_SEARCH:
+ // ignore HEADSETHOOK to allow hold-for-voice-search to work
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ return false;
+ }
+ return true;
+ }
+
+ private boolean shouldSkipKeyListener(int keyCode, KeyEvent event) {
+ if (mIMEState == IME_STATE_DISABLED ||
+ mIMEState == IME_STATE_PLUGIN) {
+ return true;
+ }
+ // Preserve enter and tab keys for the browser
+ if (keyCode == KeyEvent.KEYCODE_ENTER ||
+ keyCode == KeyEvent.KEYCODE_TAB) {
+ return true;
+ }
+ // BaseKeyListener returns false even if it handled these keys for us,
+ // so we skip the key listener entirely and handle these ourselves
+ if (keyCode == KeyEvent.KEYCODE_DEL ||
+ keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
+ return true;
+ }
+ return false;
+ }
+
+ private KeyEvent translateKey(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 &&
+ mIMEActionHint.equalsIgnoreCase("next")) {
+ return new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB);
+ }
+ break;
+ }
+
+ if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) {
+ return GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event);
+ }
+
+ return event;
+ }
+
+ // Called by OnDefaultKeyEvent handler, up from Gecko
+ /* package */ void performDefaultKeyAction(KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ case KeyEvent.KEYCODE_MEDIA_CLOSE:
+ case KeyEvent.KEYCODE_MEDIA_EJECT:
+ case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
+ // Forward media keypresses to the registered handler so headset controls work
+ // Does the same thing as Chromium
+ // https://chromium.googlesource.com/chromium/src/+/49.0.2623.67/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsDelegateAndroid.java#445
+ // These are all the keys dispatchMediaKeyEvent supports.
+ if (AppConstants.Versions.feature19Plus) {
+ // dispatchMediaKeyEvent is only available on Android 4.4+
+ Context viewContext = getView().getContext();
+ AudioManager am = (AudioManager)viewContext.getSystemService(Context.AUDIO_SERVICE);
+ am.dispatchMediaKeyEvent(event);
+ }
+ break;
+ }
+ }
+
+ private boolean processKey(final int action, final int keyCode, final KeyEvent event) {
+
+ if (keyCode > KeyEvent.getMaxKeyCode() || !shouldProcessKey(keyCode, event)) {
+ return false;
+ }
+
+ mEditableClient.postToInputConnection(new Runnable() {
+ @Override
+ public void run() {
+ sendKeyEvent(action, event);
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return processKey(KeyEvent.ACTION_DOWN, keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return processKey(KeyEvent.ACTION_UP, keyCode, event);
+ }
+
+ /**
+ * Get a key that represents a given character.
+ */
+ private KeyEvent getCharKeyEvent(final char c) {
+ final long time = SystemClock.uptimeMillis();
+ return new KeyEvent(time, time, KeyEvent.ACTION_MULTIPLE,
+ KeyEvent.KEYCODE_UNKNOWN, /* repeat */ 0) {
+ @Override
+ public int getUnicodeChar() {
+ return c;
+ }
+
+ @Override
+ public int getUnicodeChar(int metaState) {
+ return c;
+ }
+ };
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
+ // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters()
+ final String str = event.getCharacters();
+ for (int i = 0; i < str.length(); i++) {
+ final KeyEvent charEvent = getCharKeyEvent(str.charAt(i));
+ if (!processKey(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN, charEvent) ||
+ !processKey(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_UNKNOWN, charEvent)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ while ((repeatCount--) != 0) {
+ if (!processKey(KeyEvent.ACTION_DOWN, keyCode, event) ||
+ !processKey(KeyEvent.ACTION_UP, keyCode, event)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ View v = getView();
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ InputMethodManager imm = getInputMethodManager();
+ imm.toggleSoftInputFromWindow(v.getWindowToken(),
+ InputMethodManager.SHOW_FORCED, 0);
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isIMEEnabled() {
+ // make sure this picks up PASSWORD and PLUGIN states as well
+ return mIMEState != IME_STATE_DISABLED;
+ }
+
+ @Override
+ public void notifyIME(int type) {
+ switch (type) {
+
+ case NOTIFY_IME_OF_FOCUS:
+ case NOTIFY_IME_OF_BLUR:
+ // Showing/hiding vkb is done in notifyIMEContext
+ resetInputConnection();
+ break;
+
+ case NOTIFY_IME_OPEN_VKB:
+ showSoftInput();
+ break;
+
+ default:
+ if (DEBUG) {
+ throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint) {
+ // For some input type we will use a widget to display the ui, for those we must not
+ // display the ime. We can display a widget for date and time types and, if the sdk version
+ // is 11 or greater, for datetime/month/week as well.
+ if (typeHint != null &&
+ (typeHint.equalsIgnoreCase("date") ||
+ typeHint.equalsIgnoreCase("time") ||
+ (Versions.feature11Plus && (typeHint.equalsIgnoreCase("datetime") ||
+ typeHint.equalsIgnoreCase("month") ||
+ typeHint.equalsIgnoreCase("week") ||
+ typeHint.equalsIgnoreCase("datetime-local"))))) {
+ state = IME_STATE_DISABLED;
+ }
+
+ // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext,
+ // and not reset anywhere else. Usually, notifyIMEContext is called right after a
+ // focus or blur, so resetting mIMEState during the focus or blur seems harmless.
+ // However, this behavior is not guaranteed. Gecko may call notifyIMEContext
+ // independent of focus change; that is, a focus change may not be accompanied by
+ // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not
+ // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318)
+ /* When IME is 'disabled', IME processing is disabled.
+ In addition, the IME UI is hidden */
+ mIMEState = state;
+ mIMETypeHint = (typeHint == null) ? "" : typeHint;
+ mIMEModeHint = (modeHint == null) ? "" : modeHint;
+ mIMEActionHint = (actionHint == null) ? "" : actionHint;
+
+ // These fields are reset here and will be updated when restartInput is called below
+ mUpdateRequest = null;
+ mCurrentInputMethod = "";
+
+ View v = getView();
+ if (v == null || !v.hasFocus()) {
+ // When using Find In Page, we can still receive notifyIMEContext calls due to the
+ // selection changing when highlighting. However in this case we don't want to reset/
+ // show/hide the keyboard because the find box has the focus and is taking input from
+ // the keyboard.
+ return;
+ }
+ restartInput();
+ }
+}
+
+final class DebugGeckoInputConnection
+ extends GeckoInputConnection
+ implements InvocationHandler {
+
+ private InputConnection mProxy;
+ private final StringBuilder mCallLevel;
+
+ private DebugGeckoInputConnection(View targetView,
+ GeckoEditableClient editable) {
+ super(targetView, editable);
+ mCallLevel = new StringBuilder();
+ }
+
+ public static GeckoEditableListener create(View targetView,
+ GeckoEditableClient editable) {
+ final Class<?>[] PROXY_INTERFACES = { InputConnection.class,
+ InputConnectionListener.class,
+ GeckoEditableListener.class };
+ DebugGeckoInputConnection dgic =
+ new DebugGeckoInputConnection(targetView, editable);
+ dgic.mProxy = (InputConnection)Proxy.newProxyInstance(
+ GeckoInputConnection.class.getClassLoader(),
+ PROXY_INTERFACES, dgic);
+ return (GeckoEditableListener)dgic.mProxy;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+
+ StringBuilder log = new StringBuilder(mCallLevel);
+ log.append("> ").append(method.getName()).append("(");
+ if (args != null) {
+ for (Object arg : args) {
+ // translate argument values to constant names
+ if ("notifyIME".equals(method.getName()) && arg == args[0]) {
+ log.append(GeckoEditable.getConstantName(
+ GeckoEditableListener.class, "NOTIFY_IME_", arg));
+ } else if ("notifyIMEContext".equals(method.getName()) && arg == args[0]) {
+ log.append(GeckoEditable.getConstantName(
+ GeckoEditableListener.class, "IME_STATE_", arg));
+ } else {
+ GeckoEditable.debugAppend(log, arg);
+ }
+ log.append(", ");
+ }
+ if (args.length > 0) {
+ log.setLength(log.length() - 2);
+ }
+ }
+ log.append(")");
+ Log.d(LOGTAG, log.toString());
+
+ mCallLevel.append(' ');
+ Object ret = method.invoke(this, args);
+ if (ret == this) {
+ ret = mProxy;
+ }
+ mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1));
+
+ log.setLength(mCallLevel.length());
+ log.append("< ").append(method.getName());
+ if (!method.getReturnType().equals(Void.TYPE)) {
+ GeckoEditable.debugAppend(log.append(": "), ret);
+ }
+ Log.d(LOGTAG, log.toString());
+ return ret;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java
@@ -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(allowMultithread = true, stubName = "GetThreadNameJavaProfilingWrapper")
+ 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(allowMultithread = true, stubName = "GetSampleTimeJavaProfiling")
+ 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(allowMultithread = true, stubName = "GetFrameNameJavaProfilingWrapper")
+ 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(allowMultithread = true, stubName = "StartJavaProfiling")
+ 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(allowMultithread = true, stubName = "PauseJavaProfiling")
+ public static void pause() {
+ synchronized (GeckoJavaSampler.class) {
+ sSamplingRunnable.mPauseSampler = true;
+ }
+ }
+
+ @WrapForJNI(allowMultithread = true, stubName = "UnpauseJavaProfiling")
+ public static void unpause() {
+ synchronized (GeckoJavaSampler.class) {
+ sSamplingRunnable.mPauseSampler = false;
+ }
+ }
+
+ @WrapForJNI(allowMultithread = true, stubName = "StopJavaProfiling")
+ 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;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -0,0 +1,436 @@
+/* -*- 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.JNITarget;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NetworkUtils;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionSubType;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionType;
+import org.mozilla.gecko.util.NetworkUtils.NetworkStatus;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.DhcpInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.text.format.Formatter;
+import android.util.Log;
+
+/**
+ * Provides connection type, subtype and general network status (up/down).
+ *
+ * According to spec of Network Information API version 3, connection types include:
+ * bluetooth, cellular, ethernet, none, wifi and other. The objective of providing such general
+ * connection is due to some security concerns. In short, we don't want to expose exact network type,
+ * especially the cellular network type.
+ *
+ * Specific mobile subtypes are mapped to general 2G, 3G and 4G buckets.
+ *
+ * Logic is implemented as a state machine, so see the transition matrix to figure out what happens when.
+ * This class depends on access to the context, so only use after GeckoAppShell has been initialized.
+ */
+public class GeckoNetworkManager extends BroadcastReceiver implements NativeEventListener {
+ private static final String LOGTAG = "GeckoNetworkManager";
+
+ private static final String LINK_DATA_CHANGED = "changed";
+
+ private static GeckoNetworkManager instance;
+
+ public static void destroy() {
+ if (instance != null) {
+ instance.onDestroy();
+ instance = null;
+ }
+ }
+
+ public enum ManagerState {
+ OffNoListeners,
+ OffWithListeners,
+ OnNoListeners,
+ OnWithListeners
+ }
+
+ public enum ManagerEvent {
+ start,
+ stop,
+ enableNotifications,
+ disableNotifications,
+ receivedUpdate
+ }
+
+ private ManagerState currentState = ManagerState.OffNoListeners;
+ private ConnectionType currentConnectionType = ConnectionType.NONE;
+ private NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
+ private ConnectionSubType currentConnectionSubtype = ConnectionSubType.UNKNOWN;
+
+ private enum InfoType {
+ MCC,
+ MNC
+ }
+
+ private GeckoNetworkManager() {
+ EventDispatcher.getInstance().registerGeckoThreadListener(this,
+ "Wifi:Enable",
+ "Wifi:GetIPAddress");
+ }
+
+ private void onDestroy() {
+ handleManagerEvent(ManagerEvent.stop);
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+ "Wifi:Enable",
+ "Wifi:GetIPAddress");
+ }
+
+ public static GeckoNetworkManager getInstance() {
+ if (instance == null) {
+ instance = new GeckoNetworkManager();
+ }
+
+ return instance;
+ }
+
+ public double[] getCurrentInformation() {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ final ConnectionType connectionType = currentConnectionType;
+ return new double[] {
+ connectionType.value,
+ connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
+ connectionType == ConnectionType.WIFI ? wifiDhcpGatewayAddress(applicationContext) : 0.0
+ };
+ }
+
+ @Override
+ public void onReceive(Context aContext, Intent aIntent) {
+ handleManagerEvent(ManagerEvent.receivedUpdate);
+ }
+
+ public void start() {
+ handleManagerEvent(ManagerEvent.start);
+ }
+
+ public void stop() {
+ handleManagerEvent(ManagerEvent.stop);
+ }
+
+ public void enableNotifications() {
+ handleManagerEvent(ManagerEvent.enableNotifications);
+ }
+
+ public void disableNotifications() {
+ handleManagerEvent(ManagerEvent.disableNotifications);
+ }
+
+ /**
+ * For a given event, figure out the next state, run any transition by-product actions, and switch
+ * current state to the next state. If event is invalid for the current state, this is a no-op.
+ *
+ * @param event Incoming event
+ * @return Boolean indicating if transition was performed.
+ */
+ private synchronized boolean handleManagerEvent(ManagerEvent event) {
+ final ManagerState nextState = getNextState(currentState, event);
+
+ Log.d(LOGTAG, "Incoming event " + event + " for state " + currentState + " -> " + nextState);
+ if (nextState == null) {
+ Log.w(LOGTAG, "Invalid event " + event + " for state " + currentState);
+ return false;
+ }
+
+ performActionsForStateEvent(currentState, event);
+ currentState = nextState;
+
+ return true;
+ }
+
+ /**
+ * Defines a transition matrix for our state machine. For a given state/event pair, returns nextState.
+ *
+ * @param currentState Current state against which we have an incoming event
+ * @param event Incoming event for which we'd like to figure out the next state
+ * @return State into which we should transition as result of given event
+ */
+ @Nullable
+ public static ManagerState getNextState(@NonNull ManagerState currentState, @NonNull ManagerEvent event) {
+ switch (currentState) {
+ case OffNoListeners:
+ switch (event) {
+ case start:
+ return ManagerState.OnNoListeners;
+ case enableNotifications:
+ return ManagerState.OffWithListeners;
+ default:
+ return null;
+ }
+ case OnNoListeners:
+ switch (event) {
+ case stop:
+ return ManagerState.OffNoListeners;
+ case enableNotifications:
+ return ManagerState.OnWithListeners;
+ case receivedUpdate:
+ return ManagerState.OnNoListeners;
+ default:
+ return null;
+ }
+ case OnWithListeners:
+ switch (event) {
+ case stop:
+ return ManagerState.OffWithListeners;
+ case disableNotifications:
+ return ManagerState.OnNoListeners;
+ case receivedUpdate:
+ return ManagerState.OnWithListeners;
+ default:
+ return null;
+ }
+ case OffWithListeners:
+ switch (event) {
+ case start:
+ return ManagerState.OnWithListeners;
+ case disableNotifications:
+ return ManagerState.OffNoListeners;
+ default:
+ return null;
+ }
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /**
+ * For a given state/event combination, run any actions which are by-products of leaving the state
+ * because of a given event. Since this is a deterministic state machine, we can easily do that
+ * without any additional information.
+ *
+ * @param currentState State which we are leaving
+ * @param event Event which is causing us to leave the state
+ */
+ private void performActionsForStateEvent(ManagerState currentState, ManagerEvent event) {
+ // NB: network state might be queried via getCurrentInformation at any time; pre-rewrite behaviour was
+ // that network state was updated whenever enableNotifications was called. To avoid deviating
+ // from previous behaviour and causing weird side-effects, we call updateNetworkStateAndConnectionType
+ // whenever notifications are enabled.
+ switch (currentState) {
+ case OffNoListeners:
+ if (event == ManagerEvent.start) {
+ updateNetworkStateAndConnectionType();
+ registerBroadcastReceiver();
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType();
+ }
+ break;
+ case OnNoListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType();
+ sendNetworkStateToListeners();
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType();
+ registerBroadcastReceiver();
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver();
+ }
+ break;
+ case OnWithListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType();
+ sendNetworkStateToListeners();
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver();
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ case OffWithListeners:
+ if (event == ManagerEvent.start) {
+ registerBroadcastReceiver();
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /**
+ * Update current network state and connection types.
+ */
+ private void updateNetworkStateAndConnectionType() {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ final ConnectivityManager connectivityManager = (ConnectivityManager) applicationContext.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ // Type/status getters below all have a defined behaviour for when connectivityManager == null
+ if (connectivityManager == null) {
+ Log.e(LOGTAG, "ConnectivityManager does not exist.");
+ }
+ currentConnectionType = NetworkUtils.getConnectionType(connectivityManager);
+ currentNetworkStatus = NetworkUtils.getNetworkStatus(connectivityManager);
+ currentConnectionSubtype = NetworkUtils.getConnectionSubType(connectivityManager);
+ Log.d(LOGTAG, "New network state: " + currentNetworkStatus + ", " + currentConnectionType + ", " + currentConnectionSubtype);
+ }
+
+ /**
+ * Send current network state and connection type as a GeckoEvent, to whomever is listening.
+ */
+ private void sendNetworkStateToListeners() {
+ if (GeckoThread.isRunning()) {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ GeckoAppShell.sendEventToGecko(
+ GeckoEvent.createNetworkEvent(
+ currentConnectionType.value,
+ currentConnectionType == ConnectionType.WIFI,
+ wifiDhcpGatewayAddress(applicationContext),
+ currentConnectionSubtype.value
+ )
+ );
+
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(currentNetworkStatus.value));
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(LINK_DATA_CHANGED));
+ }
+ }
+
+ /**
+ * Stop listening for network state updates.
+ */
+ private void unregisterBroadcastReceiver() {
+ GeckoAppShell.getApplicationContext().unregisterReceiver(this);
+ }
+
+ /**
+ * Start listening for network state updates.
+ */
+ private void registerBroadcastReceiver() {
+ final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ if (GeckoAppShell.getApplicationContext().registerReceiver(this, filter) == null) {
+ Log.e(LOGTAG, "Registering receiver failed");
+ }
+ }
+
+ private static int wifiDhcpGatewayAddress(Context context) {
+ if (context == null) {
+ return 0;
+ }
+
+ try {
+ WifiManager mgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ DhcpInfo d = mgr.getDhcpInfo();
+ if (d == null) {
+ return 0;
+ }
+
+ return d.gateway;
+
+ } catch (Exception ex) {
+ // getDhcpInfo() is not documented to require any permissions, but on some devices
+ // requires android.permission.ACCESS_WIFI_STATE. Just catch the generic exception
+ // here and returning 0. Not logging because this could be noisy.
+ return 0;
+ }
+ }
+
+ @Override
+ /**
+ * Handles native messages, not part of the state machine flow.
+ */
+ public void handleMessage(final String event, final NativeJSObject message,
+ final EventCallback callback) {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ switch (event) {
+ case "Wifi:Enable":
+ final WifiManager mgr = (WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE);
+
+ if (!mgr.isWifiEnabled()) {
+ mgr.setWifiEnabled(true);
+ } else {
+ // If Wifi is enabled, maybe you need to select a network
+ Intent intent = new Intent(android.provider.Settings.ACTION_WIFI_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ applicationContext.startActivity(intent);
+ }
+ break;
+ case "Wifi:GetIPAddress":
+ getWifiIPAddress(callback);
+ break;
+ }
+ }
+
+ // This function only works for IPv4
+ private void getWifiIPAddress(final EventCallback callback) {
+ final WifiManager mgr = (WifiManager) GeckoAppShell.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+
+ if (mgr == null) {
+ callback.sendError("Cannot get WifiManager");
+ return;
+ }
+
+ final WifiInfo info = mgr.getConnectionInfo();
+ if (info == null) {
+ callback.sendError("Cannot get connection info");
+ return;
+ }
+
+ int ip = info.getIpAddress();
+ if (ip == 0) {
+ callback.sendError("Cannot get IPv4 address");
+ return;
+ }
+ callback.sendSuccess(Formatter.formatIpAddress(ip));
+ }
+
+ private static int getNetworkOperator(InfoType type, Context context) {
+ if (null == context) {
+ return -1;
+ }
+
+ TelephonyManager tel = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tel == null) {
+ Log.e(LOGTAG, "Telephony service does not exist");
+ return -1;
+ }
+
+ String networkOperator = tel.getNetworkOperator();
+ if (networkOperator == null || networkOperator.length() <= 3) {
+ return -1;
+ }
+
+ if (type == InfoType.MNC) {
+ return Integer.parseInt(networkOperator.substring(3));
+ }
+
+ if (type == InfoType.MCC) {
+ return Integer.parseInt(networkOperator.substring(0, 3));
+ }
+
+ return -1;
+ }
+
+ /**
+ * These are called from JavaScript ctypes. Avoid letting ProGuard delete them.
+ *
+ * Note that these methods must only be called after GeckoAppShell has been
+ * initialized: they depend on access to the context.
+ */
+ @JNITarget
+ public static int getMCC() {
+ return getNetworkOperator(InfoType.MCC, GeckoAppShell.getApplicationContext());
+ }
+
+ @JNITarget
+ public static int getMNC() {
+ return getNetworkOperator(InfoType.MNC, GeckoAppShell.getApplicationContext());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
@@ -0,0 +1,943 @@
+/* -*- 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.SharedPreferences;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
+import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.util.FileUtils;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+import org.mozilla.gecko.util.IntentUtils;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class GeckoProfile {
+ private static final String LOGTAG = "GeckoProfile";
+
+ // The path in the profile to the file containing the client ID.
+ private static final String CLIENT_ID_FILE_PATH = "datareporting/state.json";
+ private static final String FHR_CLIENT_ID_FILE_PATH = "healthreport/state.json";
+ // In the client ID file, the attribute title in the JSON object containing the client ID value.
+ private static final String CLIENT_ID_JSON_ATTR = "clientID";
+
+ private static final String TIMES_PATH = "times.json";
+ private static final String PROFILE_CREATION_DATE_JSON_ATTR = "created";
+
+ // Only tests should need to do this.
+ // We can default this to AppConstants.RELEASE_BUILD once we fix Bug 1069687.
+ private static volatile boolean sAcceptDirectoryChanges = true;
+
+ @RobocopTarget
+ public static void enableDirectoryChanges() {
+ Log.w(LOGTAG, "Directory changes should only be enabled for tests. And even then it's a bad idea.");
+ sAcceptDirectoryChanges = true;
+ }
+
+ public static final String DEFAULT_PROFILE = "default";
+ // Profile is using a custom directory outside of the Mozilla directory.
+ public static final String CUSTOM_PROFILE = "";
+ public static final String GUEST_PROFILE_DIR = "guest";
+ public static final String GUEST_MODE_PREF = "guestMode";
+
+ public static final String PREF_FIRSTRUN_ENABLED = "startpane_enabled";
+
+ // Session store
+ private static final String SESSION_FILE = "sessionstore.js";
+ private static final String SESSION_FILE_BACKUP = "sessionstore.bak";
+ private static final long MAX_BACKUP_FILE_AGE = 1000 * 3600 * 24; // 24 hours
+
+ private boolean mOldSessionDataProcessed = false;
+
+ private static final ConcurrentHashMap<String, GeckoProfile> sProfileCache =
+ new ConcurrentHashMap<String, GeckoProfile>(
+ /* capacity */ 4, /* load factor */ 0.75f, /* concurrency */ 2);
+ private static String sDefaultProfileName;
+
+ private final String mName;
+ private final File mMozillaDir;
+ private final Context mApplicationContext;
+
+ /**
+ * Access to this member should be synchronized to avoid
+ * races during creation -- particularly between getDir and GeckoView#init.
+ *
+ * Not final because this is lazily computed.
+ */
+ private File mProfileDir;
+
+ private Boolean mInGuestMode;
+
+ public static GeckoProfile initFromArgs(final Context context, final String args) {
+ if (shouldUse(context)) {
+ final GeckoProfile guestProfile = getGuestProfile(context);
+ if (guestProfile != null) {
+ return guestProfile;
+ }
+ // Failed to create guest profile; leave guest mode.
+ leave(context);
+ }
+
+ // We never want to use the guest mode profile concurrently with a normal profile
+ // -- no syncing to it, no dual-profile usage, nothing. GeckoThread startup with
+ // a conventional GeckoProfile will cause the guest profile to be deleted and
+ // guest mode to reset.
+ if (getGuestDir(context).isDirectory()) {
+ final GeckoProfile guestProfile = getGuestProfile(context);
+ if (guestProfile != null) {
+ removeProfile(context, guestProfile);
+ }
+ }
+
+ String profileName = null;
+ String profilePath = null;
+
+ if (args != null && args.contains("-P")) {
+ final Pattern p = Pattern.compile("(?:-P\\s*)(\\w*)(\\s*)");
+ final Matcher m = p.matcher(args);
+ if (m.find()) {
+ profileName = m.group(1);
+ }
+ }
+
+ if (args != null && args.contains("-profile")) {
+ final Pattern p = Pattern.compile("(?:-profile\\s*)(\\S*)(\\s*)");
+ final Matcher m = p.matcher(args);
+ if (m.find()) {
+ profilePath = m.group(1);
+ }
+ }
+
+ if (profileName == null && profilePath == null) {
+ // Get the default profile for the Activity.
+ return getDefaultProfile(context);
+ }
+
+ return GeckoProfile.get(context, profileName, profilePath);
+ }
+
+ private static GeckoProfile getDefaultProfile(Context context) {
+ try {
+ return get(context, getDefaultProfileName(context));
+
+ } catch (final NoMozillaDirectoryException e) {
+ // If this failed, we're screwed.
+ Log.wtf(LOGTAG, "Unable to get default profile name.", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static GeckoProfile get(Context context) {
+ return get(context, null, (File) null);
+ }
+
+ public static GeckoProfile get(Context context, String profileName) {
+ if (profileName != null) {
+ GeckoProfile profile = sProfileCache.get(profileName);
+ if (profile != null)
+ return profile;
+ }
+ return get(context, profileName, (File)null);
+ }
+
+ @RobocopTarget
+ public static GeckoProfile get(Context context, String profileName, String profilePath) {
+ File dir = null;
+ if (!TextUtils.isEmpty(profilePath)) {
+ dir = new File(profilePath);
+ if (!dir.exists() || !dir.isDirectory()) {
+ Log.w(LOGTAG, "requested profile directory missing: " + profilePath);
+ }
+ }
+ return get(context, profileName, dir);
+ }
+
+ // Note that the profile cache respects only the profile name!
+ // If the directory changes, the returned GeckoProfile instance will be mutated.
+ @RobocopTarget
+ public static GeckoProfile get(Context context, String profileName, File profileDir) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must be non-null");
+ }
+
+ // Null name? | Null dir? | Returned profile
+ // ------------------------------------------
+ // Yes | Yes | Active profile or default profile.
+ // No | Yes | Profile with specified name at default dir.
+ // Yes | No | Custom (anonymous) profile with specified dir.
+ // No | No | Profile with specified name at specified dir.
+
+ if (profileName == null && profileDir == null) {
+ // If no profile info was passed in, look for the active profile or a default profile.
+ final GeckoProfile profile = GeckoThread.getActiveProfile();
+ if (profile != null) {
+ return profile;
+ }
+
+ final String args;
+ if (context instanceof Activity) {
+ args = IntentUtils.getStringExtraSafe(((Activity) context).getIntent(), "args");
+ } else {
+ args = null;
+ }
+
+ return GeckoProfile.initFromArgs(context, args);
+
+ } else if (profileName == null) {
+ // If only profile dir was passed in, use custom (anonymous) profile.
+ profileName = CUSTOM_PROFILE;
+
+ } else if (AppConstants.DEBUG_BUILD) {
+ Log.v(LOGTAG, "Fetching profile: '" + profileName + "', '" + profileDir + "'");
+ }
+
+ // We require the profile dir to exist if specified, so create it here if needed.
+ final boolean init = profileDir != null && profileDir.mkdirs();
+
+ // Actually try to look up the profile.
+ GeckoProfile profile = sProfileCache.get(profileName);
+ GeckoProfile newProfile = null;
+
+ if (profile == null) {
+ try {
+ newProfile = new GeckoProfile(context, profileName, profileDir);
+ } catch (NoMozillaDirectoryException e) {
+ // We're unable to do anything sane here.
+ throw new RuntimeException(e);
+ }
+
+ profile = sProfileCache.putIfAbsent(profileName, newProfile);
+ }
+
+ if (profile == null) {
+ profile = newProfile;
+
+ } else if (profileDir != null) {
+ // We have an existing profile but was given an alternate directory.
+ boolean consistent = false;
+ try {
+ consistent = profile.mProfileDir != null &&
+ profile.mProfileDir.getCanonicalPath().equals(profileDir.getCanonicalPath());
+ } catch (final IOException e) {
+ }
+
+ if (!consistent) {
+ if (!sAcceptDirectoryChanges || !profileDir.isDirectory()) {
+ throw new IllegalStateException(
+ "Refusing to reuse profile with a different directory.");
+ }
+
+ if (AppConstants.RELEASE_BUILD) {
+ Log.e(LOGTAG, "Release build trying to switch out profile dir. " +
+ "This is an error, but let's do what we can.");
+ }
+ profile.setDir(profileDir);
+ }
+ }
+
+ if (init) {
+ // Initialize the profile directory if we had to create it.
+ profile.enqueueInitialization(profileDir);
+ }
+
+ return profile;
+ }
+
+ // Currently unused outside of testing.
+ @RobocopTarget
+ public static boolean removeProfile(final Context context, final GeckoProfile profile) {
+ final boolean success = profile.remove();
+
+ if (success) {
+ // Clear all shared prefs for the given profile.
+ GeckoSharedPrefs.forProfileName(context, profile.getName())
+ .edit().clear().apply();
+ }
+
+ return success;
+ }
+
+ private static File getGuestDir(final Context context) {
+ return context.getFileStreamPath(GUEST_PROFILE_DIR);
+ }
+
+ @RobocopTarget
+ public static GeckoProfile getGuestProfile(final Context context) {
+ return get(context, CUSTOM_PROFILE, getGuestDir(context));
+ }
+
+ public static boolean isGuestProfile(final Context context, final String profileName,
+ final File profileDir) {
+ // Guest profile is just a custom profile with a special path.
+ if (profileDir == null || !CUSTOM_PROFILE.equals(profileName)) {
+ return false;
+ }
+
+ try {
+ return profileDir.getCanonicalPath().equals(getGuestDir(context).getCanonicalPath());
+ } catch (final IOException e) {
+ return false;
+ }
+ }
+
+ private GeckoProfile(Context context, String profileName, File profileDir) throws NoMozillaDirectoryException {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Unable to create GeckoProfile for empty profile name.");
+ } else if (CUSTOM_PROFILE.equals(profileName) && profileDir == null) {
+ throw new IllegalArgumentException("Custom profile must have a directory");
+ }
+
+ mApplicationContext = context.getApplicationContext();
+ mName = profileName;
+ mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
+
+ mProfileDir = profileDir;
+ if (profileDir != null && !profileDir.isDirectory()) {
+ throw new IllegalArgumentException("Profile directory must exist if specified.");
+ }
+ }
+
+ public static boolean shouldUse(final Context context) {
+ return GeckoSharedPrefs.forApp(context).getBoolean(GUEST_MODE_PREF, false);
+ }
+
+ public static void enter(final Context context) {
+ GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, true).commit();
+ }
+
+ public static void leave(final Context context) {
+ GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, false).commit();
+ }
+
+ private void setDir(File dir) {
+ if (dir != null && dir.exists() && dir.isDirectory()) {
+ synchronized (this) {
+ mProfileDir = dir;
+ mInGuestMode = null;
+ }
+ }
+ }
+
+ @RobocopTarget
+ public String getName() {
+ return mName;
+ }
+
+ public boolean isCustomProfile() {
+ return CUSTOM_PROFILE.equals(mName);
+ }
+
+ @RobocopTarget
+ public boolean inGuestMode() {
+ if (mInGuestMode == null) {
+ mInGuestMode = isGuestProfile(GeckoAppShell.getApplicationContext(),
+ mName, mProfileDir);
+ }
+ return mInGuestMode;
+ }
+
+ /**
+ * Retrieves the directory backing the profile. This method acts
+ * as a lazy initializer for the GeckoProfile instance.
+ */
+ @RobocopTarget
+ public synchronized File getDir() {
+ forceCreate();
+ return mProfileDir;
+ }
+
+ /**
+ * Forces profile creation. Consider using {@link #getDir()} to initialize the profile instead - it is the
+ * lazy initializer and, for our code reasoning abilities, we should initialize the profile in one place.
+ */
+ private synchronized GeckoProfile forceCreate() {
+ if (mProfileDir != null) {
+ return this;
+ }
+
+ try {
+ // Check if a profile with this name already exists.
+ try {
+ mProfileDir = findProfileDir();
+ Log.d(LOGTAG, "Found profile dir.");
+ } catch (NoSuchProfileException noSuchProfile) {
+ // If it doesn't exist, create it.
+ mProfileDir = createProfileDir();
+ }
+ } catch (IOException ioe) {
+ Log.e(LOGTAG, "Error getting profile dir", ioe);
+ }
+ return this;
+ }
+
+ public File getFile(String aFile) {
+ File f = getDir();
+ if (f == null)
+ return null;
+
+ return new File(f, aFile);
+ }
+
+ /**
+ * Retrieves the Gecko client ID from the filesystem. If the client ID does not exist, we attempt to migrate and
+ * persist it from FHR and, if that fails, we attempt to create a new one ourselves.
+ *
+ * This method assumes the client ID is located in a file at a hard-coded path within the profile. The format of
+ * this file is a JSONObject which at the bottom level contains a String -> String mapping containing the client ID.
+ *
+ * WARNING: the platform provides a JSM to retrieve the client ID [1] and this would be a
+ * robust way to access it. However, we don't want to rely on Gecko running in order to get
+ * the client ID so instead we access the file this module accesses directly. However, it's
+ * possible the format of this file (and the access calls in the jsm) will change, leaving
+ * this code to fail. There are tests in TestGeckoProfile to verify the file format but be
+ * warned: THIS IS NOT FOOLPROOF.
+ *
+ * [1]: https://mxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm
+ *
+ * @throws IOException if the client ID could not be retrieved.
+ */
+ // Mimics ClientID.jsm – _doLoadClientID.
+ @WorkerThread
+ public String getClientId() throws IOException {
+ try {
+ return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
+ } catch (final IOException e) {
+ // Avoid log spam: don't log the full Exception w/ the stack trace.
+ Log.d(LOGTAG, "Could not get client ID - attempting to migrate ID from FHR: " + e.getLocalizedMessage());
+ }
+
+ String clientIdToWrite;
+ try {
+ clientIdToWrite = getValidClientIdFromDisk(FHR_CLIENT_ID_FILE_PATH);
+ } catch (final IOException e) {
+ // Avoid log spam: don't log the full Exception w/ the stack trace.
+ Log.d(LOGTAG, "Could not migrate client ID from FHR – creating a new one: " + e.getLocalizedMessage());
+ clientIdToWrite = generateNewClientId();
+ }
+
+ // There is a possibility Gecko is running and the Gecko telemetry implementation decided it's time to generate
+ // the client ID, writing client ID underneath us. Since it's highly unlikely (e.g. we run in onStart before
+ // Gecko is started), we don't handle that possibility besides writing the ID and then reading from the file
+ // again (rather than just returning the value we generated before writing).
+ //
+ // In the event it does happen, any discrepancy will be resolved after a restart. In the mean time, both this
+ // implementation and the Gecko implementation could upload documents with inconsistent IDs.
+ //
+ // In any case, if we get an exception, intentionally throw - there's nothing more to do here.
+ persistClientId(clientIdToWrite);
+ return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
+ }
+
+ protected static String generateNewClientId() {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * @return a valid client ID
+ * @throws IOException if a valid client ID could not be retrieved
+ */
+ @WorkerThread
+ private String getValidClientIdFromDisk(final String filePath) throws IOException {
+ final JSONObject obj = readJSONObjectFromFile(filePath);
+ final String clientId = obj.optString(CLIENT_ID_JSON_ATTR);
+ if (isClientIdValid(clientId)) {
+ return clientId;
+ }
+ throw new IOException("Received client ID is invalid: " + clientId);
+ }
+
+ /**
+ * Persists the given client ID to disk. This will overwrite any existing files.
+ */
+ @WorkerThread
+ private void persistClientId(final String clientId) throws IOException {
+ if (!ensureParentDirs(CLIENT_ID_FILE_PATH)) {
+ throw new IOException("Could not create client ID parent directories");
+ }
+
+ final JSONObject obj = new JSONObject();
+ try {
+ obj.put(CLIENT_ID_JSON_ATTR, clientId);
+ } catch (final JSONException e) {
+ throw new IOException("Could not create client ID JSON object", e);
+ }
+
+ // ClientID.jsm overwrites the file to store the client ID so it's okay if we do it too.
+ Log.d(LOGTAG, "Attempting to write new client ID");
+ writeFile(CLIENT_ID_FILE_PATH, obj.toString()); // Logs errors within function: ideally we'd throw.
+ }
+
+ // From ClientID.jsm - isValidClientID.
+ public static boolean isClientIdValid(final String clientId) {
+ // We could use UUID.fromString but, for consistency, we take the implementation from ClientID.jsm.
+ if (TextUtils.isEmpty(clientId)) {
+ return false;
+ }
+ return clientId.matches("(?i:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})");
+ }
+
+ /**
+ * Gets the profile creation date and persists it if it had to be generated.
+ *
+ * To get this value, we first look in times.json. If that could not be accessed, we
+ * return the package's first install date. This is not a perfect solution because a
+ * user may have large gap between install time and first use.
+ *
+ * A more correct algorithm could be the one performed by the JS code in ProfileAge.jsm
+ * getOldestProfileTimestamp: walk the tree and return the oldest timestamp on the files
+ * within the profile. However, since times.json will only not exist for the small
+ * number of really old profiles, we're okay with the package install date compromise for
+ * simplicity.
+ *
+ * @return the profile creation date in the format returned by {@link System#currentTimeMillis()}
+ * or -1 if the value could not be persisted.
+ */
+ @WorkerThread
+ public long getAndPersistProfileCreationDate(final Context context) {
+ try {
+ return getProfileCreationDateFromTimesFile();
+ } catch (final IOException e) {
+ Log.d(LOGTAG, "Unable to retrieve profile creation date from times.json. Getting from system...");
+ final long packageInstallMillis = org.mozilla.gecko.util.ContextUtils.getCurrentPackageInfo(context).firstInstallTime;
+ try {
+ persistProfileCreationDateToTimesFile(packageInstallMillis);
+ } catch (final IOException ioEx) {
+ // We return -1 to ensure the profileCreationDate
+ // will either be an error (-1) or a consistent value.
+ Log.w(LOGTAG, "Unable to persist profile creation date - returning -1");
+ return -1;
+ }
+
+ return packageInstallMillis;
+ }
+ }
+
+ @WorkerThread
+ private long getProfileCreationDateFromTimesFile() throws IOException {
+ final JSONObject obj = readJSONObjectFromFile(TIMES_PATH);
+ try {
+ return obj.getLong(PROFILE_CREATION_DATE_JSON_ATTR);
+ } catch (final JSONException e) {
+ // Don't log to avoid leaking data in JSONObject.
+ throw new IOException("Profile creation does not exist in JSONObject");
+ }
+ }
+
+ @WorkerThread
+ private void persistProfileCreationDateToTimesFile(final long profileCreationMillis) throws IOException {
+ final JSONObject obj = new JSONObject();
+ try {
+ obj.put(PROFILE_CREATION_DATE_JSON_ATTR, profileCreationMillis);
+ } catch (final JSONException e) {
+ // Don't log to avoid leaking data in JSONObject.
+ throw new IOException("Unable to persist profile creation date to times file");
+ }
+ Log.d(LOGTAG, "Attempting to write new profile creation date");
+ writeFile(TIMES_PATH, obj.toString()); // Ideally we'd throw here too.
+ }
+
+ /**
+ * Updates the state of the old session data file.
+ *
+ * sessionstore.js should hold the current session, and sessionstore.bak should
+ * hold the previous session (where it is used to read the "tabs from last time").
+ * If we're not restoring tabs automatically, sessionstore.js needs to be moved to
+ * sessionstore.bak, so we can display the correct "tabs from last time".
+ * If we *are* restoring tabs, we need to delete outdated copies of sessionstore.bak,
+ * so we don't continue showing stale "tabs from last time" indefinitely.
+ *
+ * @param shouldRestore Pass true if we are automatically restoring last session's tabs.
+ */
+ public void updateSessionFile(boolean shouldRestore) {
+ File sessionFileBackup = getFile(SESSION_FILE_BACKUP);
+ if (!shouldRestore) {
+ File sessionFile = getFile(SESSION_FILE);
+ if (sessionFile != null && sessionFile.exists()) {
+ sessionFile.renameTo(sessionFileBackup);
+ }
+ } else {
+ if (sessionFileBackup != null && sessionFileBackup.exists() &&
+ System.currentTimeMillis() - sessionFileBackup.lastModified() > MAX_BACKUP_FILE_AGE) {
+ sessionFileBackup.delete();
+ }
+ }
+ synchronized (this) {
+ mOldSessionDataProcessed = true;
+ notifyAll();
+ }
+ }
+
+ public void waitForOldSessionDataProcessing() {
+ synchronized (this) {
+ while (!mOldSessionDataProcessed) {
+ try {
+ wait();
+ } catch (final InterruptedException e) {
+ // Ignore and wait again.
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the string from a session file.
+ *
+ * The session can either be read from sessionstore.js or sessionstore.bak.
+ * In general, sessionstore.js holds the current session, and
+ * sessionstore.bak holds the previous session.
+ *
+ * @param readBackup if true, the session is read from sessionstore.bak;
+ * otherwise, the session is read from sessionstore.js
+ *
+ * @return the session string
+ */
+ public String readSessionFile(boolean readBackup) {
+ File sessionFile = getFile(readBackup ? SESSION_FILE_BACKUP : SESSION_FILE);
+
+ try {
+ if (sessionFile != null && sessionFile.exists()) {
+ return readFile(sessionFile);
+ }
+ } catch (IOException ioe) {
+ Log.e(LOGTAG, "Unable to read session file", ioe);
+ }
+ return null;
+ }
+
+ /**
+ * Ensures the parent director(y|ies) of the given filename exist by making them
+ * if they don't already exist..
+ *
+ * @param filename The path to the file whose parents should be made directories
+ * @return true if the parent directory exists, false otherwise
+ */
+ @WorkerThread
+ protected boolean ensureParentDirs(final String filename) {
+ final File file = new File(getDir(), filename);
+ final File parentFile = file.getParentFile();
+ return parentFile.mkdirs() || parentFile.isDirectory();
+ }
+
+ public void writeFile(final String filename, final String data) {
+ File file = new File(getDir(), filename);
+ BufferedWriter bufferedWriter = null;
+ try {
+ bufferedWriter = new BufferedWriter(new FileWriter(file, false));
+ bufferedWriter.write(data);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Unable to write to file", e);
+ } finally {
+ try {
+ if (bufferedWriter != null) {
+ bufferedWriter.close();
+ }
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error closing writer while writing to file", e);
+ }
+ }
+ }
+
+ @WorkerThread
+ public JSONObject readJSONObjectFromFile(final String filename) throws IOException {
+ final String fileContents;
+ try {
+ fileContents = readFile(filename);
+ } catch (final IOException e) {
+ // Don't log exception to avoid leaking profile path.
+ throw new IOException("Could not access given file to retrieve JSONObject");
+ }
+
+ try {
+ return new JSONObject(fileContents);
+ } catch (final JSONException e) {
+ // Don't log exception to avoid leaking profile path.
+ throw new IOException("Could not parse JSON to retrieve JSONObject");
+ }
+ }
+
+ public JSONArray readJSONArrayFromFile(final String filename) {
+ String fileContent;
+ try {
+ fileContent = readFile(filename);
+ } catch (IOException expected) {
+ return new JSONArray();
+ }
+
+ JSONArray jsonArray;
+ try {
+ jsonArray = new JSONArray(fileContent);
+ } catch (JSONException e) {
+ jsonArray = new JSONArray();
+ }
+ return jsonArray;
+ }
+
+ public String readFile(String filename) throws IOException {
+ File dir = getDir();
+ if (dir == null) {
+ throw new IOException("No profile directory found");
+ }
+ File target = new File(dir, filename);
+ return readFile(target);
+ }
+
+ private String readFile(File target) throws IOException {
+ FileReader fr = new FileReader(target);
+ try {
+ StringBuilder sb = new StringBuilder();
+ char[] buf = new char[8192];
+ int read = fr.read(buf);
+ while (read >= 0) {
+ sb.append(buf, 0, read);
+ read = fr.read(buf);
+ }
+ return sb.toString();
+ } finally {
+ fr.close();
+ }
+ }
+
+ public boolean deleteFileFromProfileDir(String fileName) throws IllegalArgumentException {
+ if (TextUtils.isEmpty(fileName)) {
+ throw new IllegalArgumentException("Filename cannot be empty.");
+ }
+ File file = new File(getDir(), fileName);
+ return file.delete();
+ }
+
+ private boolean remove() {
+ try {
+ synchronized (this) {
+ if (mProfileDir != null && mProfileDir.exists()) {
+ FileUtils.delete(mProfileDir);
+ }
+
+ if (isCustomProfile()) {
+ // Custom profiles don't have profile.ini sections that we need to remove.
+ return true;
+ }
+
+ try {
+ // If findProfileDir() succeeds, it means the profile was created
+ // through forceCreate(), so we set mProfileDir to null to enable
+ // forceCreate() to create the profile again.
+ findProfileDir();
+ mProfileDir = null;
+
+ } catch (final NoSuchProfileException e) {
+ // If findProfileDir() throws, it means the profile was not created
+ // through forceCreate(), and we have to preserve mProfileDir because
+ // it was given to us. In that case, there's nothing left to do here.
+ return true;
+ }
+ }
+
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
+ final Hashtable<String, INISection> sections = parser.getSections();
+ for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
+ final INISection section = e.nextElement();
+ String name = section.getStringProperty("Name");
+
+ if (name == null || !name.equals(mName)) {
+ continue;
+ }
+
+ if (section.getName().startsWith("Profile")) {
+ // ok, we have stupid Profile#-named things. Rename backwards.
+ try {
+ int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length()));
+ String curSection = "Profile" + sectionNumber;
+ String nextSection = "Profile" + (sectionNumber + 1);
+
+ sections.remove(curSection);
+
+ while (sections.containsKey(nextSection)) {
+ parser.renameSection(nextSection, curSection);
+ sectionNumber++;
+
+ curSection = nextSection;
+ nextSection = "Profile" + (sectionNumber + 1);
+ }
+ } catch (NumberFormatException nex) {
+ // uhm, malformed Profile thing; we can't do much.
+ Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName());
+ return false;
+ }
+ } else {
+ // this really shouldn't be the case, but handle it anyway
+ parser.removeSection(mName);
+ }
+
+ break;
+ }
+
+ parser.write();
+ return true;
+ } catch (IOException ex) {
+ Log.w(LOGTAG, "Failed to remove profile.", ex);
+ return false;
+ }
+ }
+
+ /**
+ * @return the default profile name for this application, or
+ * {@link GeckoProfile#DEFAULT_PROFILE} if none could be found.
+ *
+ * @throws NoMozillaDirectoryException
+ * if the Mozilla directory did not exist and could not be
+ * created.
+ */
+ public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
+ // Have we read the default profile from the INI already?
+ // Changing the default profile requires a restart, so we don't
+ // need to worry about runtime changes.
+ if (sDefaultProfileName != null) {
+ return sDefaultProfileName;
+ }
+
+ final String profileName = GeckoProfileDirectories.findDefaultProfileName(context);
+ if (profileName == null) {
+ // Note that we don't persist this back to profiles.ini.
+ sDefaultProfileName = DEFAULT_PROFILE;
+ return DEFAULT_PROFILE;
+ }
+
+ sDefaultProfileName = profileName;
+ return sDefaultProfileName;
+ }
+
+ private File findProfileDir() throws NoSuchProfileException {
+ if (isCustomProfile()) {
+ return mProfileDir;
+ }
+ return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
+ }
+
+ @WorkerThread
+ private File createProfileDir() throws IOException {
+ if (isCustomProfile()) {
+ // Custom profiles must already exist.
+ return mProfileDir;
+ }
+
+ INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
+
+ // Salt the name of our requested profile
+ String saltedName = GeckoProfileDirectories.saltProfileName(mName);
+ File profileDir = new File(mMozillaDir, saltedName);
+ while (profileDir.exists()) {
+ saltedName = GeckoProfileDirectories.saltProfileName(mName);
+ profileDir = new File(mMozillaDir, saltedName);
+ }
+
+ // Attempt to create the salted profile dir
+ if (!profileDir.mkdirs()) {
+ throw new IOException("Unable to create profile.");
+ }
+ Log.d(LOGTAG, "Created new profile dir.");
+
+ // Now update profiles.ini
+ // If this is the first time its created, we also add a General section
+ // look for the first profile number that isn't taken yet
+ int profileNum = 0;
+ boolean isDefaultSet = false;
+ INISection profileSection;
+ while ((profileSection = parser.getSection("Profile" + profileNum)) != null) {
+ profileNum++;
+ if (profileSection.getProperty("Default") != null) {
+ isDefaultSet = true;
+ }
+ }
+
+ profileSection = new INISection("Profile" + profileNum);
+ profileSection.setProperty("Name", mName);
+ profileSection.setProperty("IsRelative", 1);
+ profileSection.setProperty("Path", saltedName);
+
+ if (parser.getSection("General") == null) {
+ INISection generalSection = new INISection("General");
+ generalSection.setProperty("StartWithLastProfile", 1);
+ parser.addSection(generalSection);
+ }
+
+ if (!isDefaultSet) {
+ // only set as default if this is the first profile we're creating
+ profileSection.setProperty("Default", 1);
+
+ // We have no intention of stopping this session. The FIRSTRUN session
+ // ends when the browsing session/activity has ended. All events
+ // during firstrun will be tagged as FIRSTRUN.
+ // TODO: local broadcast to tell App that things are happening?
+ // Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
+ }
+
+ parser.addSection(profileSection);
+ parser.write();
+
+ enqueueInitialization(profileDir);
+
+ // Write out profile creation time, mirroring the logic in nsToolkitProfileService.
+ try {
+ FileOutputStream stream = new FileOutputStream(profileDir.getAbsolutePath() + File.separator + TIMES_PATH);
+ OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
+ try {
+ writer.append("{\"created\": " + System.currentTimeMillis() + "}\n");
+ } finally {
+ writer.close();
+ }
+ } catch (Exception e) {
+ // Best-effort.
+ Log.w(LOGTAG, "Couldn't write " + TIMES_PATH, e);
+ }
+
+ // Create the client ID file before Gecko starts (we assume this method
+ // is called before Gecko starts). If we let Gecko start, the JS telemetry
+ // code may try to write to the file at the same time Java does.
+ persistClientId(generateNewClientId());
+
+ // Initialize pref flag for displaying the start pane for a new profile.
+ final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplicationContext);
+ prefs.edit().putBoolean(PREF_FIRSTRUN_ENABLED, true).apply();
+
+ return profileDir;
+ }
+
+ /**
+ * This method is called once, immediately before creation of the profile
+ * directory completes.
+ *
+ * It queues up work to be done in the background to prepare the profile,
+ * such as adding default bookmarks.
+ *
+ * This is public for use *from tests only*!
+ */
+ @RobocopTarget
+ public void enqueueInitialization(final File profileDir) {
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+
+import android.content.Context;
+
+/**
+ * <code>GeckoProfileDirectories</code> manages access to mappings from profile
+ * names to salted profile directory paths, as well as the default profile name.
+ *
+ * This class will eventually come to encapsulate the remaining logic embedded
+ * in profiles.ini; for now it's a read-only wrapper.
+ */
+public class GeckoProfileDirectories {
+ @SuppressWarnings("serial")
+ public static class NoMozillaDirectoryException extends Exception {
+ public NoMozillaDirectoryException(Throwable cause) {
+ super(cause);
+ }
+
+ public NoMozillaDirectoryException(String reason) {
+ super(reason);
+ }
+
+ public NoMozillaDirectoryException(String reason, Throwable cause) {
+ super(reason, cause);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class NoSuchProfileException extends Exception {
+ public NoSuchProfileException(String detailMessage, Throwable cause) {
+ super(detailMessage, cause);
+ }
+
+ public NoSuchProfileException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ private interface INISectionPredicate {
+ public boolean matches(INISection section);
+ }
+
+ private static final String MOZILLA_DIR_NAME = "mozilla";
+
+ /**
+ * Returns true if the supplied profile entry represents the default profile.
+ */
+ private static final INISectionPredicate sectionIsDefault = new INISectionPredicate() {
+ @Override
+ public boolean matches(INISection section) {
+ return section.getIntProperty("Default") == 1;
+ }
+ };
+
+ /**
+ * Returns true if the supplied profile entry has a 'Name' field.
+ */
+ private static final INISectionPredicate sectionHasName = new INISectionPredicate() {
+ @Override
+ public boolean matches(INISection section) {
+ final String name = section.getStringProperty("Name");
+ return name != null;
+ }
+ };
+
+ @RobocopTarget
+ public static INIParser getProfilesINI(File mozillaDir) {
+ return new INIParser(new File(mozillaDir, "profiles.ini"));
+ }
+
+ /**
+ * Utility method to compute a salted profile name: eight random alphanumeric
+ * characters, followed by a period, followed by the profile name.
+ */
+ public static String saltProfileName(final String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Cannot salt null profile name.");
+ }
+
+ final String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789";
+ final int scale = allowedChars.length();
+ final int saltSize = 8;
+
+ final StringBuilder saltBuilder = new StringBuilder(saltSize + 1 + name.length());
+ for (int i = 0; i < saltSize; i++) {
+ saltBuilder.append(allowedChars.charAt((int)(Math.random() * scale)));
+ }
+ saltBuilder.append('.');
+ saltBuilder.append(name);
+ return saltBuilder.toString();
+ }
+
+ /**
+ * Return the Mozilla directory within the files directory of the provided
+ * context. This should always be the same within a running application.
+ *
+ * This method is package-scoped so that new {@link GeckoProfile} instances can
+ * contextualize themselves.
+ *
+ * @return a new File object for the Mozilla directory.
+ * @throws NoMozillaDirectoryException
+ * if the directory did not exist and could not be created.
+ */
+ @RobocopTarget
+ public static File getMozillaDirectory(Context context) throws NoMozillaDirectoryException {
+ final File mozillaDir = new File(context.getFilesDir(), MOZILLA_DIR_NAME);
+ if (mozillaDir.mkdirs() || mozillaDir.isDirectory()) {
+ return mozillaDir;
+ }
+
+ // Although this leaks a path to the system log, the path is
+ // predictable (unlike a profile directory), so this is fine.
+ throw new NoMozillaDirectoryException("Unable to create mozilla directory at " + mozillaDir.getAbsolutePath());
+ }
+
+ /**
+ * Discover the default profile name by examining profiles.ini.
+ *
+ * Package-scoped because {@link GeckoProfile} needs access to it.
+ *
+ * @return null if there is no "Default" entry in profiles.ini, or the profile
+ * name if there is.
+ * @throws NoMozillaDirectoryException
+ * if the Mozilla directory did not exist and could not be created.
+ */
+ static String findDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(getMozillaDirectory(context));
+
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
+ final INISection section = e.nextElement();
+ if (section.getIntProperty("Default") == 1) {
+ return section.getStringProperty("Name");
+ }
+ }
+
+ return null;
+ }
+
+ static Map<String, String> getDefaultProfile(final File mozillaDir) {
+ return getMatchingProfiles(mozillaDir, sectionIsDefault, true);
+ }
+
+ static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) {
+ final INISectionPredicate predicate = new INISectionPredicate() {
+ @Override
+ public boolean matches(final INISection section) {
+ return name.equals(section.getStringProperty("Name"));
+ }
+ };
+ return getMatchingProfiles(mozillaDir, predicate, true);
+ }
+
+ /**
+ * Calls {@link GeckoProfileDirectories#getMatchingProfiles(File, INISectionPredicate, boolean)}
+ * with a filter to ensure that all profiles are named.
+ */
+ static Map<String, String> getAllProfiles(final File mozillaDir) {
+ return getMatchingProfiles(mozillaDir, sectionHasName, false);
+ }
+
+ /**
+ * Return a mapping from the names of all matching profiles (that is,
+ * profiles appearing in profiles.ini that match the supplied predicate) to
+ * their absolute paths on disk.
+ *
+ * @param mozillaDir
+ * a directory containing profiles.ini.
+ * @param predicate
+ * a predicate to use when evaluating whether to include a
+ * particular INI section.
+ * @param stopOnSuccess
+ * if true, this method will return with the first result that
+ * matches the predicate; if false, all matching results are
+ * included.
+ * @return a {@link Map} from name to path.
+ */
+ public static Map<String, String> getMatchingProfiles(final File mozillaDir, INISectionPredicate predicate, boolean stopOnSuccess) {
+ final HashMap<String, String> result = new HashMap<String, String>();
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
+
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
+ final INISection section = e.nextElement();
+ if (predicate == null || predicate.matches(section)) {
+ final String name = section.getStringProperty("Name");
+ final String pathString = section.getStringProperty("Path");
+ final boolean isRelative = section.getIntProperty("IsRelative") == 1;
+ final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
+ result.put(name, path.getAbsolutePath());
+
+ if (stopOnSuccess) {
+ return result;
+ }
+ }
+ }
+ return result;
+ }
+
+ public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException {
+ // Open profiles.ini to find the correct path.
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
+
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
+ final INISection section = e.nextElement();
+ final String name = section.getStringProperty("Name");
+ if (name != null && name.equals(profileName)) {
+ if (section.getIntProperty("IsRelative") == 1) {
+ return new File(mozillaDir, section.getStringProperty("Path"));
+ }
+ return new File(section.getStringProperty("Path"));
+ }
+ }
+
+ throw new NoSuchProfileException("No profile " + profileName);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
@@ -0,0 +1,414 @@
+/* -*- 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.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.util.Log;
+import android.view.Surface;
+import android.app.Activity;
+
+import java.util.Arrays;
+import java.util.List;
+
+/*
+ * Updates, locks and unlocks the screen orientation.
+ *
+ * Note: Replaces the OnOrientationChangeListener to avoid redundant rotation
+ * event handling.
+ */
+public class GeckoScreenOrientation {
+ private static final String LOGTAG = "GeckoScreenOrientation";
+
+ // Make sure that any change in dom/base/ScreenOrientation.h happens here too.
+ public enum ScreenOrientation {
+ NONE(0),
+ PORTRAIT_PRIMARY(1 << 0),
+ PORTRAIT_SECONDARY(1 << 1),
+ PORTRAIT(PORTRAIT_PRIMARY.value | PORTRAIT_SECONDARY.value),
+ LANDSCAPE_PRIMARY(1 << 2),
+ LANDSCAPE_SECONDARY(1 << 3),
+ LANDSCAPE(LANDSCAPE_PRIMARY.value | LANDSCAPE_SECONDARY.value),
+ DEFAULT(1 << 4);
+
+ public final short value;
+
+ private ScreenOrientation(int value) {
+ this.value = (short)value;
+ }
+
+ private final static ScreenOrientation[] sValues = ScreenOrientation.values();
+
+ public static ScreenOrientation get(int value) {
+ for (ScreenOrientation orient: sValues) {
+ if (orient.value == value) {
+ return orient;
+ }
+ }
+ return NONE;
+ }
+ }
+
+ // Singleton instance.
+ private static GeckoScreenOrientation sInstance;
+ // Default screen orientation, used for initialization and unlocking.
+ private static final ScreenOrientation DEFAULT_SCREEN_ORIENTATION = ScreenOrientation.DEFAULT;
+ // Default rotation, used when device rotation is unknown.
+ private static final int DEFAULT_ROTATION = Surface.ROTATION_0;
+ // Default orientation, used if screen orientation is unspecified.
+ private ScreenOrientation mDefaultScreenOrientation;
+ // Last updated screen orientation.
+ private ScreenOrientation mScreenOrientation;
+ // Whether the update should notify Gecko about screen orientation changes.
+ private boolean mShouldNotify = true;
+ // Configuration screen orientation preference path.
+ private static final String DEFAULT_SCREEN_ORIENTATION_PREF = "app.orientation.default";
+
+ public GeckoScreenOrientation() {
+ PrefsHelper.getPref(DEFAULT_SCREEN_ORIENTATION_PREF, new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, String value) {
+ // Read and update the configuration default preference.
+ mDefaultScreenOrientation = screenOrientationFromArrayString(value);
+ setRequestedOrientation(mDefaultScreenOrientation);
+ }
+ });
+
+ mDefaultScreenOrientation = DEFAULT_SCREEN_ORIENTATION;
+ update();
+ }
+
+ public static GeckoScreenOrientation getInstance() {
+ if (sInstance == null) {
+ sInstance = new GeckoScreenOrientation();
+ }
+ return sInstance;
+ }
+
+ /*
+ * Enable Gecko screen orientation events on update.
+ */
+ public void enableNotifications() {
+ update();
+ mShouldNotify = true;
+ }
+
+ /*
+ * Disable Gecko screen orientation events on update.
+ */
+ public void disableNotifications() {
+ mShouldNotify = false;
+ }
+
+ /*
+ * Update screen orientation.
+ * Retrieve orientation and rotation via GeckoAppShell.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ return false;
+ }
+ Configuration config = activity.getResources().getConfiguration();
+ return update(config.orientation);
+ }
+
+ /*
+ * Update screen orientation given the android orientation.
+ * Retrieve rotation via GeckoAppShell.
+ *
+ * @param aAndroidOrientation
+ * Android screen orientation from Configuration.orientation.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update(int aAndroidOrientation) {
+ return update(getScreenOrientation(aAndroidOrientation, getRotation()));
+ }
+
+ /*
+ * Update screen orientation given the screen orientation.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation based on android orientation and rotation.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update(ScreenOrientation aScreenOrientation) {
+ if (mScreenOrientation == aScreenOrientation) {
+ return false;
+ }
+ mScreenOrientation = aScreenOrientation;
+ Log.d(LOGTAG, "updating to new orientation " + mScreenOrientation);
+ if (mShouldNotify) {
+ // Gecko expects a definite screen orientation, so we default to the
+ // primary orientations.
+ if (aScreenOrientation == ScreenOrientation.PORTRAIT) {
+ aScreenOrientation = ScreenOrientation.PORTRAIT_PRIMARY;
+ } else if (aScreenOrientation == ScreenOrientation.LANDSCAPE) {
+ aScreenOrientation = ScreenOrientation.LANDSCAPE_PRIMARY;
+ }
+ GeckoAppShell.sendEventToGecko(
+ GeckoEvent.createScreenOrientationEvent(aScreenOrientation.value,
+ getAngle()));
+ }
+ GeckoAppShell.resetScreenSize();
+ return true;
+ }
+
+ /*
+ * @return The Android orientation (Configuration.orientation).
+ */
+ public int getAndroidOrientation() {
+ return screenOrientationToAndroidOrientation(getScreenOrientation());
+ }
+
+ /*
+ * @return The Gecko screen orientation derived from Android orientation and
+ * rotation.
+ */
+ public ScreenOrientation getScreenOrientation() {
+ return mScreenOrientation;
+ }
+
+ /*
+ * Lock screen orientation given the Gecko screen orientation.
+ *
+ * @param aGeckoOrientation
+ * The Gecko orientation provided.
+ */
+ public void lock(int aGeckoOrientation) {
+ lock(ScreenOrientation.get(aGeckoOrientation));
+ }
+
+ /*
+ * Lock screen orientation given the Gecko screen orientation.
+ * Retrieve rotation via GeckoAppShell.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation derived from Android orientation and
+ * rotation.
+ *
+ * @return Whether the locking was successful.
+ */
+ public boolean lock(ScreenOrientation aScreenOrientation) {
+ Log.d(LOGTAG, "locking to " + aScreenOrientation);
+ update(aScreenOrientation);
+ return setRequestedOrientation(aScreenOrientation);
+ }
+
+ /*
+ * Unlock and update screen orientation.
+ *
+ * @return Whether the unlocking was successful.
+ */
+ public boolean unlock() {
+ Log.d(LOGTAG, "unlocking");
+ setRequestedOrientation(mDefaultScreenOrientation);
+ return update();
+ }
+
+ private Activity getActivity() {
+ if (GeckoAppShell.getGeckoInterface() == null) {
+ return null;
+ }
+ return GeckoAppShell.getGeckoInterface().getActivity();
+ }
+
+ /*
+ * Set the given requested orientation for the current activity.
+ * This is essentially an unlock without an update.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ *
+ * @return Whether the requested orientation was set. This can only fail if
+ * the current activity cannot be retrieved via GeckoAppShell.
+ *
+ */
+ private boolean setRequestedOrientation(ScreenOrientation aScreenOrientation) {
+ int activityOrientation = screenOrientationToActivityInfoOrientation(aScreenOrientation);
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.w(LOGTAG, "setRequestOrientation: failed to get activity");
+ return false;
+ }
+ if (activity.getRequestedOrientation() == activityOrientation) {
+ return false;
+ }
+ activity.setRequestedOrientation(activityOrientation);
+ return true;
+ }
+
+ /*
+ * Combine the Android orientation and rotation to the Gecko orientation.
+ *
+ * @param aAndroidOrientation
+ * Android orientation from Configuration.orientation.
+ * @param aRotation
+ * Device rotation from Display.getRotation().
+ *
+ * @return Gecko screen orientation.
+ */
+ private ScreenOrientation getScreenOrientation(int aAndroidOrientation, int aRotation) {
+ boolean isPrimary = aRotation == Surface.ROTATION_0 || aRotation == Surface.ROTATION_90;
+ if (aAndroidOrientation == Configuration.ORIENTATION_PORTRAIT) {
+ if (isPrimary) {
+ // Non-rotated portrait device or landscape device rotated
+ // to primary portrait mode counter-clockwise.
+ return ScreenOrientation.PORTRAIT_PRIMARY;
+ }
+ return ScreenOrientation.PORTRAIT_SECONDARY;
+ }
+ if (aAndroidOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+ if (isPrimary) {
+ // Non-rotated landscape device or portrait device rotated
+ // to primary landscape mode counter-clockwise.
+ return ScreenOrientation.LANDSCAPE_PRIMARY;
+ }
+ return ScreenOrientation.LANDSCAPE_SECONDARY;
+ }
+ return ScreenOrientation.NONE;
+ }
+
+ /*
+ * @return Device rotation converted to an angle.
+ */
+ public short getAngle() {
+ switch (getRotation()) {
+ case Surface.ROTATION_0:
+ return 0;
+ case Surface.ROTATION_90:
+ return 90;
+ case Surface.ROTATION_180:
+ return 180;
+ case Surface.ROTATION_270:
+ return 270;
+ default:
+ Log.w(LOGTAG, "getAngle: unexpected rotation value");
+ return 0;
+ }
+ }
+
+ /*
+ * @return Device rotation from Display.getRotation().
+ */
+ private int getRotation() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.w(LOGTAG, "getRotation: failed to get activity");
+ return DEFAULT_ROTATION;
+ }
+ return activity.getWindowManager().getDefaultDisplay().getRotation();
+ }
+
+ /*
+ * Retrieve the screen orientation from an array string.
+ *
+ * @param aArray
+ * String containing comma-delimited strings.
+ *
+ * @return First parsed Gecko screen orientation.
+ */
+ public static ScreenOrientation screenOrientationFromArrayString(String aArray) {
+ List<String> orientations = Arrays.asList(aArray.split(","));
+ if (orientations.size() == 0) {
+ // If nothing is listed, return default.
+ Log.w(LOGTAG, "screenOrientationFromArrayString: no orientation in string");
+ return DEFAULT_SCREEN_ORIENTATION;
+ }
+
+ // We don't support multiple orientations yet. To avoid developer
+ // confusion, just take the first one listed.
+ return screenOrientationFromString(orientations.get(0));
+ }
+
+ /*
+ * Retrieve the screen orientation from a string.
+ *
+ * @param aStr
+ * String hopefully containing a screen orientation name.
+ * @return Gecko screen orientation if matched, DEFAULT_SCREEN_ORIENTATION
+ * otherwise.
+ */
+ public static ScreenOrientation screenOrientationFromString(String aStr) {
+ switch (aStr) {
+ case "portrait":
+ return ScreenOrientation.PORTRAIT;
+ case "landscape":
+ return ScreenOrientation.LANDSCAPE;
+ case "portrait-primary":
+ return ScreenOrientation.PORTRAIT_PRIMARY;
+ case "portrait-secondary":
+ return ScreenOrientation.PORTRAIT_SECONDARY;
+ case "landscape-primary":
+ return ScreenOrientation.LANDSCAPE_PRIMARY;
+ case "landscape-secondary":
+ return ScreenOrientation.LANDSCAPE_SECONDARY;
+ }
+
+ Log.w(LOGTAG, "screenOrientationFromString: unknown orientation string: " + aStr);
+ return DEFAULT_SCREEN_ORIENTATION;
+ }
+
+ /*
+ * Convert Gecko screen orientation to Android orientation.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ * @return Android orientation. This conversion is lossy, the Android
+ * orientation does not differentiate between primary and secondary
+ * orientations.
+ */
+ public static int screenOrientationToAndroidOrientation(ScreenOrientation aScreenOrientation) {
+ switch (aScreenOrientation) {
+ case PORTRAIT:
+ case PORTRAIT_PRIMARY:
+ case PORTRAIT_SECONDARY:
+ return Configuration.ORIENTATION_PORTRAIT;
+ case LANDSCAPE:
+ case LANDSCAPE_PRIMARY:
+ case LANDSCAPE_SECONDARY:
+ return Configuration.ORIENTATION_LANDSCAPE;
+ case NONE:
+ case DEFAULT:
+ default:
+ return Configuration.ORIENTATION_UNDEFINED;
+ }
+ }
+
+
+ /*
+ * Convert Gecko screen orientation to Android ActivityInfo orientation.
+ * This is yet another orientation used by Android, but it's more detailed
+ * than the Android orientation.
+ * It is required for screen orientation locking and unlocking.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ * @return Android ActivityInfo orientation.
+ */
+ public static int screenOrientationToActivityInfoOrientation(ScreenOrientation aScreenOrientation) {
+ switch (aScreenOrientation) {
+ case PORTRAIT:
+ case PORTRAIT_PRIMARY:
+ return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ case PORTRAIT_SECONDARY:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ case LANDSCAPE:
+ case LANDSCAPE_PRIMARY:
+ return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ case LANDSCAPE_SECONDARY:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ case DEFAULT:
+ case NONE:
+ return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ default:
+ return ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoService.java
@@ -0,0 +1,210 @@
+/* -*- 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.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();
+ GeckoAppShell.setApplicationContext(getApplicationContext());
+ GeckoAppShell.setNotificationClient(new ServiceNotificationClient(getApplicationContext()));
+ 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 || profileDir == null) {
+ throw new IllegalArgumentException("Intent must specify profile.");
+ }
+
+ if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
+ new File(profileDir))) {
+ 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;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java
@@ -0,0 +1,318 @@
+/* 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.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.StrictMode;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/**
+ * {@code GeckoSharedPrefs} provides scoped SharedPreferences instances.
+ * You should use this API instead of using Context.getSharedPreferences()
+ * directly. There are four methods to get scoped SharedPreferences instances:
+ *
+ * forApp()
+ * Use it for app-wide, cross-profile pref keys.
+ * forCrashReporter()
+ * For the crash reporter, which runs in its own process.
+ * forProfile()
+ * Use it to fetch and store keys for the current profile.
+ * forProfileName()
+ * Use it to fetch and store keys from/for a specific profile.
+ *
+ * {@code GeckoSharedPrefs} has a notion of migrations. Migrations can used to
+ * migrate keys from one scope to another. You can trigger a new migration by
+ * incrementing PREFS_VERSION and updating migrateIfNecessary() accordingly.
+ *
+ * Migration history:
+ * 1: Move all PreferenceManager keys to app/profile scopes
+ * 2: Move the crash reporter's private preferences into their own scope
+ */
+@RobocopTarget
+public final class GeckoSharedPrefs {
+ private static final String LOGTAG = "GeckoSharedPrefs";
+
+ // Increment it to trigger a new migration
+ public static final int PREFS_VERSION = 2;
+
+ // Name for app-scoped prefs
+ public static final String APP_PREFS_NAME = "GeckoApp";
+
+ // Name for crash reporter prefs
+ public static final String CRASH_PREFS_NAME = "CrashReporter";
+
+ // Used when fetching profile-scoped prefs.
+ public static final String PROFILE_PREFS_NAME_PREFIX = "GeckoProfile-";
+
+ // The prefs key that holds the current migration
+ private static final String PREFS_VERSION_KEY = "gecko_shared_prefs_migration";
+
+ // For disabling migration when getting a SharedPreferences instance
+ private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS);
+
+ // The keys that have to be moved from ProfileManager's default
+ // shared prefs to the profile from version 0 to 1.
+ private static final String[] PROFILE_MIGRATIONS_0_TO_1 = {
+ "home_panels",
+ "home_locale"
+ };
+
+ // The keys that have to be moved from the app prefs
+ // into the crash reporter's own prefs.
+ private static final String[] PROFILE_MIGRATIONS_1_TO_2 = {
+ "sendReport",
+ "includeUrl",
+ "allowContact",
+ "contactEmail"
+ };
+
+ // For optimizing the migration check in subsequent get() calls
+ private static volatile boolean migrationDone;
+
+ public enum Flags {
+ DISABLE_MIGRATIONS
+ }
+
+ public static SharedPreferences forApp(Context context) {
+ return forApp(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns an app-scoped SharedPreferences instance. You can disable
+ * migrations by using the DISABLE_MIGRATIONS flag.
+ */
+ public static SharedPreferences forApp(Context context, EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ return context.getSharedPreferences(APP_PREFS_NAME, 0);
+ }
+
+ public static SharedPreferences forCrashReporter(Context context) {
+ return forCrashReporter(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns a crash-reporter-scoped SharedPreferences instance. You can disable
+ * migrations by using the DISABLE_MIGRATIONS flag.
+ */
+ public static SharedPreferences forCrashReporter(Context context, EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ return context.getSharedPreferences(CRASH_PREFS_NAME, 0);
+ }
+
+ public static SharedPreferences forProfile(Context context) {
+ return forProfile(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns a SharedPreferences instance scoped to the current profile
+ * in the app. You can disable migrations by using the DISABLE_MIGRATIONS
+ * flag.
+ */
+ public static SharedPreferences forProfile(Context context, EnumSet<Flags> flags) {
+ String profileName = GeckoProfile.get(context).getName();
+ if (profileName == null) {
+ throw new IllegalStateException("Could not get current profile name");
+ }
+
+ return forProfileName(context, profileName, flags);
+ }
+
+ public static SharedPreferences forProfileName(Context context, String profileName) {
+ return forProfileName(context, profileName, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns an SharedPreferences instance scoped to the given profile name.
+ * You can disable migrations by using the DISABLE_MIGRATION flag.
+ */
+ public static SharedPreferences forProfileName(Context context, String profileName,
+ EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ final String prefsName = PROFILE_PREFS_NAME_PREFIX + profileName;
+ return context.getSharedPreferences(prefsName, 0);
+ }
+
+ /**
+ * Returns the current version of the prefs.
+ */
+ public static int getVersion(Context context) {
+ return forApp(context, disableMigrations).getInt(PREFS_VERSION_KEY, 0);
+ }
+
+ /**
+ * Resets migration flag. Should only be used in tests.
+ */
+ public static synchronized void reset() {
+ migrationDone = false;
+ }
+
+ /**
+ * Performs all prefs migrations in the background thread to avoid StrictMode
+ * exceptions from reading/writing in the UI thread. This method will block
+ * the current thread until the migration is finished.
+ */
+ private static synchronized void migrateIfNecessary(final Context context) {
+ if (migrationDone) {
+ return;
+ }
+
+ // We deliberately perform the migration in the current thread (which
+ // is likely the UI thread) as this is actually cheaper than enforcing a
+ // context switch to another thread (see bug 940575).
+ // Avoid strict mode warnings when doing so.
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ StrictMode.allowThreadDiskWrites();
+ try {
+ performMigration(context);
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+
+ migrationDone = true;
+ }
+
+ private static void performMigration(Context context) {
+ final SharedPreferences appPrefs = forApp(context, disableMigrations);
+
+ final int currentVersion = appPrefs.getInt(PREFS_VERSION_KEY, 0);
+ Log.d(LOGTAG, "Current version = " + currentVersion + ", prefs version = " + PREFS_VERSION);
+
+ if (currentVersion == PREFS_VERSION) {
+ return;
+ }
+
+ Log.d(LOGTAG, "Performing migration");
+
+ final Editor appEditor = appPrefs.edit();
+
+ // The migration always moves prefs to the default profile, not
+ // the current one. We might have to revisit this if we ever support
+ // multiple profiles.
+ final String defaultProfileName;
+ try {
+ defaultProfileName = GeckoProfile.getDefaultProfileName(context);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to get default profile name for migration");
+ }
+
+ final Editor profileEditor = forProfileName(context, defaultProfileName, disableMigrations).edit();
+ final Editor crashEditor = forCrashReporter(context, disableMigrations).edit();
+
+ List<String> profileKeys;
+ Editor pmEditor = null;
+
+ for (int v = currentVersion + 1; v <= PREFS_VERSION; v++) {
+ Log.d(LOGTAG, "Migrating to version = " + v);
+
+ switch (v) {
+ case 1:
+ profileKeys = Arrays.asList(PROFILE_MIGRATIONS_0_TO_1);
+ pmEditor = migrateFromPreferenceManager(context, appEditor, profileEditor, profileKeys);
+ break;
+ case 2:
+ profileKeys = Arrays.asList(PROFILE_MIGRATIONS_1_TO_2);
+ migrateCrashReporterSettings(appPrefs, appEditor, crashEditor, profileKeys);
+ break;
+ }
+ }
+
+ // Update prefs version accordingly.
+ appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION);
+
+ appEditor.apply();
+ profileEditor.apply();
+ crashEditor.apply();
+ if (pmEditor != null) {
+ pmEditor.apply();
+ }
+
+ Log.d(LOGTAG, "All keys have been migrated");
+ }
+
+ /**
+ * Moves all preferences stored in PreferenceManager's default prefs
+ * to either app or profile scopes. The profile-scoped keys are defined
+ * in given profileKeys list, all other keys are moved to the app scope.
+ */
+ public static Editor migrateFromPreferenceManager(Context context, Editor appEditor,
+ Editor profileEditor, List<String> profileKeys) {
+ Log.d(LOGTAG, "Migrating from PreferenceManager");
+
+ final SharedPreferences pmPrefs =
+ PreferenceManager.getDefaultSharedPreferences(context);
+
+ for (Map.Entry<String, ?> entry : pmPrefs.getAll().entrySet()) {
+ final String key = entry.getKey();
+
+ final Editor to;
+ if (profileKeys.contains(key)) {
+ to = profileEditor;
+ } else {
+ to = appEditor;
+ }
+
+ putEntry(to, key, entry.getValue());
+ }
+
+ // Clear PreferenceManager's prefs once we're done
+ // and return the Editor to be committed.
+ return pmPrefs.edit().clear();
+ }
+
+ /**
+ * Moves the crash reporter's preferences from the app-wide prefs
+ * into its own shared prefs to avoid cross-process pref accesses.
+ */
+ public static void migrateCrashReporterSettings(SharedPreferences appPrefs, Editor appEditor,
+ Editor crashEditor, List<String> profileKeys) {
+ Log.d(LOGTAG, "Migrating crash reporter settings");
+
+ for (Map.Entry<String, ?> entry : appPrefs.getAll().entrySet()) {
+ final String key = entry.getKey();
+
+ if (profileKeys.contains(key)) {
+ putEntry(crashEditor, key, entry.getValue());
+ appEditor.remove(key);
+ }
+ }
+ }
+
+ private static void putEntry(Editor to, String key, Object value) {
+ Log.d(LOGTAG, "Migrating key = " + key + " with value = " + value);
+
+ if (value instanceof String) {
+ to.putString(key, (String) value);
+ } else if (value instanceof Boolean) {
+ to.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Long) {
+ to.putLong(key, (Long) value);
+ } else if (value instanceof Float) {
+ to.putFloat(key, (Float) value);
+ } else if (value instanceof Integer) {
+ to.putInt(key, (Integer) value);
+ } else {
+ throw new IllegalStateException("Unrecognized value type for key: " + key);
+ }
+ }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSmsManager.java
@@ -0,0 +1,1244 @@
+/* -*- 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 android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.provider.Telephony;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.util.Log;
+
+import static android.telephony.SmsMessage.MessageClass;
+import static org.mozilla.gecko.SmsManager.ISmsManager;
+
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The envelope class contains all information that are needed to keep track of
+ * a sent SMS.
+ */
+class Envelope
+{
+ enum SubParts {
+ SENT_PART,
+ DELIVERED_PART
+ }
+
+ protected int mId;
+ protected int mMessageId;
+ protected long mMessageTimestamp;
+
+ /**
+ * Number of sent/delivered remaining parts.
+ * @note The array has much slots as SubParts items.
+ */
+ protected int[] mRemainingParts;
+
+ /**
+ * Whether sending/delivering is currently failing.
+ * @note The array has much slots as SubParts items.
+ */
+ protected boolean[] mFailing;
+
+ /**
+ * Error type (only for sent).
+ */
+ protected int mError;
+
+ public Envelope(int aId, int aParts) {
+ mId = aId;
+ mMessageId = -1;
+ mError = GeckoSmsManager.kNoError;
+
+ int size = SubParts.values().length;
+ mRemainingParts = new int[size];
+ mFailing = new boolean[size];
+
+ for (int i = 0; i < size; ++i) {
+ mRemainingParts[i] = aParts;
+ mFailing[i] = false;
+ }
+ }
+
+ public void decreaseRemainingParts(SubParts aType) {
+ --mRemainingParts[aType.ordinal()];
+
+ if (mRemainingParts[SubParts.SENT_PART.ordinal()] >
+ mRemainingParts[SubParts.DELIVERED_PART.ordinal()]) {
+ Log.e("GeckoSmsManager", "Delivered more parts than we sent!?");
+ }
+ }
+
+ public boolean arePartsRemaining(SubParts aType) {
+ return mRemainingParts[aType.ordinal()] != 0;
+ }
+
+ public void markAsFailed(SubParts aType) {
+ mFailing[aType.ordinal()] = true;
+ }
+
+ public boolean isFailing(SubParts aType) {
+ return mFailing[aType.ordinal()];
+ }
+
+ public int getMessageId() {
+ return mMessageId;
+ }
+
+ public void setMessageId(int aMessageId) {
+ mMessageId = aMessageId;
+ }
+
+ public long getMessageTimestamp() {
+ return mMessageTimestamp;
+ }
+
+ public void setMessageTimestamp(long aMessageTimestamp) {
+ mMessageTimestamp = aMessageTimestamp;
+ }
+
+ public int getError() {
+ return mError;
+ }
+
+ public void setError(int aError) {
+ mError = aError;
+ }
+}
+
+/**
+ * Postman class is a singleton that manages Envelope instances.
+ */
+class Postman
+{
+ public static final int kUnknownEnvelopeId = -1;
+
+ private static final Postman sInstance = new Postman();
+
+ private final ArrayList<Envelope> mEnvelopes = new ArrayList<>(1);
+
+ private Postman() {}
+
+ public static Postman getInstance() {
+ return sInstance;
+ }
+
+ public int createEnvelope(int aParts) {
+ /*
+ * We are going to create the envelope in the first empty slot in the array
+ * list. If there is no empty slot, we create a new one.
+ */
+ int size = mEnvelopes.size();
+
+ for (int i = 0; i < size; ++i) {
+ if (mEnvelopes.get(i) == null) {
+ mEnvelopes.set(i, new Envelope(i, aParts));
+ return i;
+ }
+ }
+
+ mEnvelopes.add(new Envelope(size, aParts));
+ return size;
+ }
+
+ public Envelope getEnvelope(int aId) {
+ if (aId < 0 || mEnvelopes.size() <= aId) {
+ Log.e("GeckoSmsManager", "Trying to get an unknown Envelope!");
+ return null;
+ }
+
+ Envelope envelope = mEnvelopes.get(aId);
+ if (envelope == null) {
+ Log.e("GeckoSmsManager", "Trying to get an empty Envelope!");
+ }
+
+ return envelope;
+ }
+
+ public void destroyEnvelope(int aId) {
+ if (aId < 0 || mEnvelopes.size() <= aId) {
+ Log.e("GeckoSmsManager", "Trying to destroy an unknown Envelope!");
+ return;
+ }
+
+ if (mEnvelopes.set(aId, null) == null) {
+ Log.e("GeckoSmsManager", "Trying to destroy an empty Envelope!");
+ }
+ }
+}
+
+class SmsIOThread extends HandlerThread {
+ private final static SmsIOThread sInstance = new SmsIOThread();
+
+ private Handler mHandler;
+
+ public static SmsIOThread getInstance() {
+ return sInstance;
+ }
+
+ SmsIOThread() {
+ super("SmsIOThread");
+ }
+
+ @Override
+ public void start() {
+ super.start();
+ mHandler = new Handler(getLooper());
+ }
+
+ public boolean execute(Runnable r) {
+ return mHandler.post(r);
+ }
+}
+
+class MessagesListManager
+{
+ private static final MessagesListManager sInstance = new MessagesListManager();
+
+ public static MessagesListManager getInstance() {
+ return sInstance;
+ }
+
+ private final HashMap<Integer, Cursor> mCursors = new HashMap<>();
+
+ public void add(int id, Cursor aCursor) {
+ if (mCursors.containsKey(id)) {
+ Log.e("GeckoSmsManager", "Trying to overwrite cursor!");
+ return;
+ }
+
+ mCursors.put(id, aCursor);
+ }
+
+ public Cursor get(int aId) {
+ if (!mCursors.containsKey(aId)) {
+ Log.e("GeckoSmsManager", "Cursor doesn't exist!");
+ return null;
+ }
+
+ return mCursors.get(aId);
+ }
+
+ public void remove(int aId) {
+ if (!mCursors.containsKey(aId)) {
+ Log.e("GeckoSmsManager", "Cursor doesn't exist!");
+ return;
+ }
+
+ mCursors.remove(aId);
+ }
+
+ public void clear() {
+ Set<Map.Entry<Integer, Cursor>> entries = mCursors.entrySet();
+ Iterator<Map.Entry<Integer, Cursor>> it = entries.iterator();
+ while (it.hasNext()) {
+ it.next().getValue().close();
+ }
+
+ mCursors.clear();
+ }
+}
+
+public class GeckoSmsManager
+ extends BroadcastReceiver
+ implements ISmsManager
+{
+ public final static String ACTION_SMS_SENT = "org.mozilla.gecko.SMS_SENT";
+ public final static String ACTION_SMS_DELIVERED = "org.mozilla.gecko.SMS_DELIVERED";
+
+ /*
+ * Make sure that the following error codes are in sync with |ErrorType| in:
+ * dom/mobilemessage/interfaces/nsIMobileMessageCallback.idl
+ * The error codes are owned by the DOM.
+ */
+ public final static int kNoError = 0;
+ public final static int kNoSignalError = 1;
+ public final static int kNotFoundError = 2;
+ public final static int kUnknownError = 3;
+ public final static int kInternalError = 4;
+ public final static int kNoSimCardError = 5;
+ public final static int kRadioDisabledError = 6;
+ public final static int kInvalidAddressError = 7;
+ public final static int kFdnCheckError = 8;
+ public final static int kNonActiveSimCardError = 9;
+ public final static int kStorageFullError = 10;
+ public final static int kSimNotMatchedError = 11;
+ public final static int kNetworkProblemsError = 12;
+ public final static int kGeneralProblemsError = 13;
+ public final static int kServiceNotAvailableError = 14;
+ public final static int kMessageTooLongForNetworkError = 15;
+ public final static int kServiceNotSupportedError = 16;
+ public final static int kRetryRequiredError = 17;
+
+ private final static int kMaxMessageSize = 160;
+
+ private final static Uri kSmsContentUri = Telephony.Sms.Inbox.CONTENT_URI;
+ private final static Uri kSmsSentContentUri = Telephony.Sms.Sent.CONTENT_URI;
+ private final static Uri kSmsThreadsContentUri = Telephony.Sms.Conversations.CONTENT_URI;
+
+ private final static int kSmsTypeInbox = Telephony.Sms.MESSAGE_TYPE_INBOX;
+ private final static int kSmsTypeSentbox = Telephony.Sms.MESSAGE_TYPE_SENT;
+
+ /*
+ * Keep the following state codes in syng with |DeliveryState| in:
+ * dom/mobilemessage/Types.h
+ */
+ private final static int kDeliveryStateSent = 0;
+ private final static int kDeliveryStateReceived = 1;
+ private final static int kDeliveryStateSending = 2;
+ private final static int kDeliveryStateError = 3;
+ private final static int kDeliveryStateUnknown = 4;
+ private final static int kDeliveryStateNotDownloaded = 5;
+ private final static int kDeliveryStateEndGuard = 6;
+
+ /*
+ * Keep the following status codes in sync with |DeliveryStatus| in:
+ * dom/mobilemessage/Types.h
+ */
+ private final static int kDeliveryStatusNotApplicable = 0;
+ private final static int kDeliveryStatusSuccess = 1;
+ private final static int kDeliveryStatusPending = 2;
+ private final static int kDeliveryStatusError = 3;
+
+ /*
+ * Keep the following values in sync with |MessageClass| in:
+ * dom/mobilemessage/Types.h
+ */
+ private final static int kMessageClassNormal = 0;
+ private final static int kMessageClassClass0 = 1;
+ private final static int kMessageClassClass1 = 2;
+ private final static int kMessageClassClass2 = 3;
+ private final static int kMessageClassClass3 = 4;
+
+ private final static String[] kRequiredMessageRows = { "_id", "address", "body", "date", "type", "status", "read", "thread_id" };
+ private final static String[] kRequiredMessageRowsForThread = { "_id", "address", "body", "read", "subject", "date" };
+ private final static String[] kThreadProjection = { "thread_id" };
+
+ // Used to generate monotonically increasing GUIDs.
+ private static final AtomicInteger pendingIntentGuid = new AtomicInteger(Integer.MIN_VALUE);
+
+ // The maximum value of a 32 bit signed integer. Used to enforce a limit on ids.
+ private static final long UNSIGNED_INTEGER_MAX_VALUE = Integer.MAX_VALUE * 2L + 1L;
+
+ public GeckoSmsManager() {
+ if (SmsIOThread.getInstance().getState() == Thread.State.NEW) {
+ SmsIOThread.getInstance().start();
+ }
+ }
+
+ private boolean isDefaultSmsApp(Context context) {
+ String myPackageName = context.getPackageName();
+ return Telephony.Sms.getDefaultSmsPackage(context).equals(myPackageName);
+ }
+
+ @Override
+ public void start() {
+ IntentFilter smsFilter = new IntentFilter();
+ smsFilter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
+ smsFilter.addAction(ACTION_SMS_SENT);
+ smsFilter.addAction(ACTION_SMS_DELIVERED);
+
+ GeckoAppShell.getContext().registerReceiver(this, smsFilter);
+ }
+
+ /**
+ * Build up the SMS message body from the SmsMessage array of received SMS
+ *
+ * @param msgs The SmsMessage array of the received SMS
+ * @return The text message body
+ */
+ private static String buildMessageBodyFromPdus(SmsMessage[] msgs) {
+ if (msgs.length == 1) {
+ // There is only one part, so grab the body directly.
+ return replaceFormFeeds(msgs[0].getDisplayMessageBody());
+ } else {
+ // Build up the body from the parts.
+ StringBuilder body = new StringBuilder();
+ for (SmsMessage msg : msgs) {
+ // getDisplayMessageBody() can NPE if mWrappedMessage inside is null.
+ body.append(msg.getDisplayMessageBody());
+ }
+ return replaceFormFeeds(body.toString());
+ }
+ }
+
+ // Some providers send formfeeds in their messages. Convert those formfeeds to newlines.
+ private static String replaceFormFeeds(String s) {
+ return s == null ? "" : s.replace('\f', '\n');
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION) ||
+ intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
+ // If we're the default SMS, ignore SMS_RECEIVED intents since we'll handle
+ // the SMS_DELIVER intent instead.
+ if (isDefaultSmsApp(GeckoAppShell.getContext()) && intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
+ return;
+ }
+
+ // TODO: Try to find the receiver number to be able to populate
+ // SmsMessage.receiver.
+ SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
+ if (messages == null || messages.length == 0) {
+ return;
+ }
+
+ SmsMessage sms = messages[0];
+ String body = buildMessageBodyFromPdus(messages);
+ long timestamp = System.currentTimeMillis();
+
+ int id = 0;
+ // We only need to save the message if we're the default SMS app
+ if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION)) {
+ id = saveReceivedMessage(context,
+ sms.getDisplayOriginatingAddress(),
+ body,
+ sms.getTimestampMillis(),
+ timestamp,
+ sms.getPseudoSubject());
+ }
+
+ notifySmsReceived(id,
+ sms.getDisplayOriginatingAddress(),
+ body,
+ getGeckoMessageClass(sms.getMessageClass()),
+ sms.getTimestampMillis(),
+ timestamp);
+
+ return;
+ }
+
+ if (intent.getAction().equals(ACTION_SMS_SENT) ||
+ intent.getAction().equals(ACTION_SMS_DELIVERED)) {
+ Bundle bundle = intent.getExtras();
+
+ if (bundle == null || !bundle.containsKey("envelopeId") ||
+ !bundle.containsKey("number") || !bundle.containsKey("message") ||
+ !bundle.containsKey("requestId")) {
+ Log.e("GeckoSmsManager", "Got an invalid ACTION_SMS_SENT/ACTION_SMS_DELIVERED!");
+ return;
+ }
+
+ int envelopeId = bundle.getInt("envelopeId");
+ Postman postman = Postman.getInstance();
+
+ Envelope envelope = postman.getEnvelope(envelopeId);
+ if (envelope == null) {
+ Log.e("GeckoSmsManager", "Got an invalid envelope id (or Envelope has been destroyed)!");
+ return;
+ }
+
+ Envelope.SubParts part = intent.getAction().equals(ACTION_SMS_SENT)
+ ? Envelope.SubParts.SENT_PART
+ : Envelope.SubParts.DELIVERED_PART;
+ envelope.decreaseRemainingParts(part);
+
+
+ if (getResultCode() != Activity.RESULT_OK) {
+ switch (getResultCode()) {
+ case SmsManager.RESULT_ERROR_NULL_PDU:
+ envelope.setError(kInternalError);
+ break;
+ case SmsManager.RESULT_ERROR_NO_SERVICE:
+ case SmsManager.RESULT_ERROR_RADIO_OFF:
+ envelope.setError(kNoSignalError);
+ break;
+ case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+ default:
+ envelope.setError(kUnknownError);
+ break;
+ }
+ envelope.markAsFailed(part);
+ Log.i("GeckoSmsManager", "SMS part sending failed!");
+ }
+
+ if (envelope.arePartsRemaining(part)) {
+ return;
+ }
+
+ if (envelope.isFailing(part)) {
+ if (part == Envelope.SubParts.SENT_PART) {
+ if (bundle.getBoolean("shouldNotify")) {
+ notifySmsSendFailed(envelope.getError(), bundle.getInt("requestId"));
+ }
+ Log.i("GeckoSmsManager", "SMS sending failed!");
+ } else {
+ notifySmsDelivery(envelope.getMessageId(),
+ kDeliveryStatusError,
+ bundle.getString("number"),
+ bundle.getString("message"),
+ envelope.getMessageTimestamp());
+ Log.i("GeckoSmsManager", "SMS delivery failed!");
+ }
+ } else {
+ if (part == Envelope.SubParts.SENT_PART) {
+ String number = bundle.getString("number");
+ String message = bundle.getString("message");
+ long timestamp = System.currentTimeMillis();
+
+ // save message only if we're default SMS app, otherwise sendTextMessage does it for us
+ int id = 0;
+ if (isDefaultSmsApp(GeckoAppShell.getContext())) {
+ id = saveSentMessage(number, message, timestamp);
+ }
+
+ if (bundle.getBoolean("shouldNotify")) {
+ notifySmsSent(id, number, message, timestamp, bundle.getInt("requestId"));
+ }
+
+ envelope.setMessageId(id);
+ envelope.setMessageTimestamp(timestamp);
+
+ Log.i("GeckoSmsManager", "SMS sending was successful!");
+ } else {
+ Uri id = ContentUris.withAppendedId(kSmsContentUri, envelope.getMessageId());
+ updateMessageStatus(id, Telephony.Sms.STATUS_COMPLETE);
+
+ notifySmsDelivery(envelope.getMessageId(),
+ kDeliveryStatusSuccess,
+ bundle.getString("number"),
+ bundle.getString("message"),
+ envelope.getMessageTimestamp());
+ Log.i("GeckoSmsManager", "SMS successfully delivered!");
+ }
+ }
+
+ // Destroy the envelope object only if the SMS has been sent and delivered.
+ if (!envelope.arePartsRemaining(Envelope.SubParts.SENT_PART) &&
+ !envelope.arePartsRemaining(Envelope.SubParts.DELIVERED_PART)) {
+ postman.destroyEnvelope(envelopeId);
+ }
+ }
+ }
+
+ @Override
+ public void send(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify) {
+ int envelopeId = Postman.kUnknownEnvelopeId;
+
+ try {
+ SmsManager sm = SmsManager.getDefault();
+
+ Intent sentIntent = new Intent(GeckoAppShell.getContext(), GeckoSmsManager.class);
+ sentIntent.setAction(ACTION_SMS_SENT);
+ Intent deliveredIntent = new Intent(GeckoAppShell.getContext(), GeckoSmsManager.class);
+ deliveredIntent.setAction(ACTION_SMS_DELIVERED);
+
+ Bundle bundle = new Bundle();
+ bundle.putString("number", aNumber);
+ bundle.putString("message", aMessage);
+ bundle.putInt("requestId", aRequestId);
+ bundle.putBoolean("shouldNotify", aShouldNotify);
+
+ if (aMessage.length() <= kMaxMessageSize) {
+ envelopeId = Postman.getInstance().createEnvelope(1);
+ bundle.putInt("envelopeId", envelopeId);
+
+ sentIntent.putExtras(bundle);
+ deliveredIntent.putExtras(bundle);
+
+ /*
+ * There are a few things to know about getBroadcast and pending intents:
+ * - the pending intents are in a shared pool maintained by the system;
+ * - each pending intent is identified by a token;
+ * - when a new pending intent is created, if it has the same token as
+ * another intent in the pool, one of them has to be removed.
+ *
+ * To prevent having a hard time because of this situation, we give a
+ * unique id to all pending intents we are creating. This unique id is
+ * generated by GetPendingIntentUID().
+ */
+ PendingIntent sentPendingIntent =
+ PendingIntent.getBroadcast(GeckoAppShell.getContext(),
+ pendingIntentGuid.incrementAndGet(), sentIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+
+ PendingIntent deliveredPendingIntent =
+ PendingIntent.getBroadcast(GeckoAppShell.getContext(),
+ pendingIntentGuid.incrementAndGet(), deliveredIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+
+ sm.sendTextMessage(aNumber, null, aMessage,
+ sentPendingIntent, deliveredPendingIntent);
+ } else {
+ ArrayList<String> parts = sm.divideMessage(aMessage);
+ envelopeId = Postman.getInstance().createEnvelope(parts.size());
+ bundle.putInt("envelopeId", envelopeId);
+
+ sentIntent.putExtras(bundle);
+ deliveredIntent.putExtras(bundle);
+
+ ArrayList<PendingIntent> sentPendingIntents =
+ new ArrayList<PendingIntent>(parts.size());
+ ArrayList<PendingIntent> deliveredPendingIntents =
+ new ArrayList<PendingIntent>(parts.size());
+
+ for (int i = 0; i < parts.size(); ++i) {
+ sentPendingIntents.add(
+ PendingIntent.getBroadcast(GeckoAppShell.getContext(),
+ pendingIntentGuid.incrementAndGet(), sentIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT)
+ );
+
+ deliveredPendingIntents.add(
+ PendingIntent.getBroadcast(GeckoAppShell.getContext(),
+ pendingIntentGuid.incrementAndGet(), deliveredIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT)
+ );
+ }
+
+ sm.sendMultipartTextMessage(aNumber, null, parts, sentPendingIntents,
+ deliveredPendingIntents);
+ }
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Failed to send an SMS: ", e);
+
+ if (envelopeId != Postman.kUnknownEnvelopeId) {
+ Postman.getInstance().destroyEnvelope(envelopeId);
+ }
+
+ notifySmsSendFailed(kUnknownError, aRequestId);
+ }
+ }
+
+ public int saveSentMessage(String aRecipient, String aBody, long aDate) {
+ try {
+ ContentValues values = new ContentValues();
+ values.put(Telephony.Sms.ADDRESS, aRecipient);
+ values.put(Telephony.Sms.BODY, aBody);
+ values.put(Telephony.Sms.DATE, aDate);
+ // Always 'PENDING' because we always request status report.
+ values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_PENDING);
+ values.put(Telephony.Sms.SEEN, 0);
+ values.put(Telephony.Sms.READ, 0);
+
+ ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+ Uri uri = cr.insert(kSmsSentContentUri, values);
+
+ long id = ContentUris.parseId(uri);
+
+ // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
+ // we happen to need more than that but it doesn't cost to check.
+ if (id > UNSIGNED_INTEGER_MAX_VALUE) {
+ throw new IdTooHighException();
+ }
+
+ return (int)id;
+ } catch (IdTooHighException e) {
+ Log.e("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
+ return -1;
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Something went wrong when trying to write a sent message", e);
+ return -1;
+ }
+ }
+
+ public void updateMessageStatus(Uri aId, int aStatus) {
+ ContentValues values = new ContentValues(1);
+ values.put(Telephony.Sms.STATUS, aStatus);
+
+ ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+ if (cr.update(aId, values, null, null) != 1) {
+ Log.i("GeckoSmsManager", "Failed to update message status!");
+ }
+ }
+
+ public int saveReceivedMessage(Context aContext, String aSender, String aBody, long aDateSent, long aDate, String aSubject) {
+ ContentValues values = new ContentValues();
+ values.put(Telephony.Sms.Inbox.ADDRESS, aSender);
+ values.put(Telephony.Sms.Inbox.BODY, aBody);
+ values.put(Telephony.Sms.Inbox.DATE_SENT, aDateSent);
+ values.put(Telephony.Sms.Inbox.DATE, aDate);
+ values.put(Telephony.Sms.Inbox.STATUS, Telephony.Sms.STATUS_COMPLETE);
+ values.put(Telephony.Sms.Inbox.READ, 0);
+ values.put(Telephony.Sms.Inbox.SEEN, 0);
+
+ ContentResolver cr = aContext.getContentResolver();
+ Uri uri = cr.insert(kSmsContentUri, values);
+
+ long id = ContentUris.parseId(uri);
+
+ // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
+ // we happen to need more than that but it doesn't cost to check.
+ if (id > UNSIGNED_INTEGER_MAX_VALUE) {
+ Log.i("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
+ return -1;
+ }
+
+ return (int)id;
+ }
+
+ @Override
+ public void getMessage(int aMessageId, int aRequestId) {
+ class GetMessageRunnable implements Runnable {
+ private final int mMessageId;
+ private final int mRequestId;
+
+ GetMessageRunnable(int aMessageId, int aRequestId) {
+ mMessageId = aMessageId;
+ mRequestId = aRequestId;
+ }
+
+ @Override
+ public void run() {
+ Cursor cursor = null;
+
+ try {
+ ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+ Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
+
+ cursor = cr.query(message, kRequiredMessageRows, null, null, null);
+ if (cursor == null || cursor.getCount() == 0) {
+ throw new NotFoundException();
+ }
+
+ if (cursor.getCount() != 1) {
+ throw new TooManyResultsException();
+ }
+
+ cursor.moveToFirst();
+
+ if (cursor.getInt(cursor.getColumnIndex("_id")) != mMessageId) {
+ throw new UnmatchingIdException();
+ }
+
+ int type = cursor.getInt(cursor.getColumnIndex("type"));
+ int deliveryStatus;
+ String sender = "";
+ String receiver = "";
+
+ if (type == kSmsTypeInbox) {
+ deliveryStatus = kDeliveryStatusSuccess;
+ sender = cursor.getString(cursor.getColumnIndex("address"));
+ } else if (type == kSmsTypeSentbox) {
+ deliveryStatus = getGeckoDeliveryStatus(cursor.getInt(cursor.getColumnIndex("status")));
+ receiver = cursor.getString(cursor.getColumnIndex("address"));
+ } else {
+ throw new InvalidTypeException();
+ }
+
+ boolean read = cursor.getInt(cursor.getColumnIndex("read")) != 0;
+
+ notifyGetSms(cursor.getInt(cursor.getColumnIndex("_id")),
+ deliveryStatus,
+ receiver, sender,
+ cursor.getString(cursor.getColumnIndex("body")),
+ cursor.getLong(cursor.getColumnIndex("date")),
+ read,
+ mRequestId);
+ } catch (NotFoundException e) {
+ Log.i("GeckoSmsManager", "Message id " + mMessageId + " not found");
+ notifyGetSmsFailed(kNotFoundError, mRequestId);
+ } catch (UnmatchingIdException e) {
+ Log.e("GeckoSmsManager", "Requested message id (" + mMessageId +
+ ") is different from the one we got.");
+ notifyGetSmsFailed(kUnknownError, mRequestId);
+ } catch (TooManyResultsException e) {
+ Log.e("GeckoSmsManager", "Get too many results for id " + mMessageId);
+ notifyGetSmsFailed(kUnknownError, mRequestId);
+ } catch (InvalidTypeException e) {
+ Log.i("GeckoSmsManager", "Message has an invalid type, we ignore it.");
+ notifyGetSmsFailed(kNotFoundError, mRequestId);
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Error while trying to get message", e);
+ notifyGetSmsFailed(kUnknownError, mRequestId);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ if (!SmsIOThread.getInstance().execute(new GetMessageRunnable(aMessageId, aRequestId))) {
+ Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
+ notifyGetSmsFailed(kUnknownError, aRequestId);
+ }
+ }
+
+ @Override
+ public void deleteMessage(int aMessageId, int aRequestId) {
+ class DeleteMessageRunnable implements Runnable {
+ private final int mMessageId;
+ private final int mRequestId;
+
+ DeleteMessageRunnable(int aMessageId, int aRequestId) {
+ mMessageId = aMessageId;
+ mRequestId = aRequestId;
+ }
+
+ @Override
+ public void run() {
+ try {
+ ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+ Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
+
+ int count = cr.delete(message, null, null);
+
+ if (count > 1) {
+ throw new TooManyResultsException();
+ }
+
+ notifySmsDeleted(count == 1, mRequestId);
+ } catch (TooManyResultsException e) {
+ Log.e("GeckoSmsManager", "Delete more than one message?", e);
+ notifySmsDeleteFailed(kUnknownError, mRequestId);
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Error while trying to delete a message", e);
+ notifySmsDeleteFailed(kUnknownError, mRequestId);
+ }
+ }
+ }
+
+ if (!SmsIOThread.getInstance().execute(new DeleteMessageRunnable(aMessageId, aRequestId))) {
+ Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
+ notifySmsDeleteFailed(kUnknownError, aRequestId);
+ }
+ }
+
+ private void getMessageFromCursorAndNotify(Cursor aCursor, int aRequestId) throws Exception {
+ int type = aCursor.getInt(aCursor.getColumnIndex("type"));
+ int deliveryStatus = kDeliveryStateUnknown;
+ String sender = "";
+ String receiver = "";
+
+ if (type == kSmsTypeInbox) {
+ deliveryStatus = kDeliveryStatusSuccess;
+ sender = aCursor.getString(aCursor.getColumnIndex("address"));
+ } else if (type == kSmsTypeSentbox) {
+ deliveryStatus = getGeckoDeliveryStatus(aCursor.getInt(aCursor.getColumnIndex("status")));
+ receiver = aCursor.getString(aCursor.getColumnIndex("address"));
+ } else {
+ throw new Exception("Shouldn't ever get a message here that's not from inbox or sentbox");
+ }
+
+ boolean read = aCursor.getInt(aCursor.getColumnIndex("read")) != 0;
+
+ notifyMessageCursorResult(aCursor.getInt(aCursor.getColumnIndex("_id")),
+ deliveryStatus,
+ receiver, sender,
+ aCursor.getString(aCursor.getColumnIndex("body")),
+ aCursor.getLong(aCursor.getColumnIndex("date")),
+ aCursor.getLong(aCursor.getColumnIndex("thread_id")),
+ read,
+ aRequestId);
+ }
+
+ @Override
+ public void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId) {
+ class MarkMessageReadRunnable implements Runnable {
+ private final int mMessageId;
+ private final boolean mValue;
+ private final int mRequestId;
+
+ MarkMessageReadRunnable(int aMessageId, boolean aValue, int aRequestId) {
+ mMessageId = aMessageId;
+ mValue = aValue;
+ mRequestId = aRequestId;
+ }
+
+ @Override
+ public void run() {
+ try {
+ ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+ Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
+
+ ContentValues updatedProps = new ContentValues();
+ updatedProps.put("read", mValue);
+
+ int count = cr.update(message, updatedProps, null, null);
+
+ notifySmsMarkedAsRead(count == 1, mRequestId);
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Error while trying to mark message as read: " + e);
+ notifySmsMarkAsReadFailed(kUnknownError, mRequestId);
+ }
+ }
+ }
+
+ if (aSendReadReport == true) {
+ Log.w("GeckoSmsManager", "Android SmsManager doesn't suport read receipts for SMS.");
+ }
+
+ if (!SmsIOThread.getInstance().execute(new MarkMessageReadRunnable(aMessageId, aValue, aRequestId))) {
+ Log.e("GeckoSmsManager", "Failed to add MarkMessageReadRunnable to the SmsIOThread");
+ notifySmsMarkAsReadFailed(kUnknownError, aRequestId);
+ }
+ }
+
+ @Override
+ public void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
+ class CreateMessageCursorRunnable implements Runnable {
+ private final long mStartDate;
+ private final long mEndDate;
+ private final String[] mNumbers;
+ private final int mNumbersCount;
+ private final String mDelivery;
+ private final boolean mHasThreadId;
+ private final long mThreadId;
+ private final boolean mReverse;
+ private final int mRequestId;
+
+ CreateMessageCursorRunnable(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId) {
+ mStartDate = aStartDate;
+ mEndDate = aEndDate;
+ mNumbers = aNumbers;
+ mNumbersCount = aNumbersCount;
+ mDelivery = aDelivery;
+ mHasThreadId = aHasThreadId;
+ mThreadId = aThreadId;
+ mReverse = aReverse;
+ mRequestId = aRequestId;
+ }
+
+ @Override
+ public void run() {
+ Cursor cursor = null;
+ boolean closeCursor = true;
+
+ try {
+ StringBuilder restrictions = new StringBuilder();
+ Formatter formatter = new Formatter(restrictions);
+
+ if (mStartDate >= 0) {
+ formatter.format("date >= '%d' AND ", mStartDate);
+ }
+
+ if (mEndDate >= 0) {
+ formatter.format("date <= '%d' AND ", mEndDate);
+ }
+
+ if (mNumbersCount > 0) {
+ formatter.format("address IN ('%s'", mNumbers[0]);
+
+ for (int i = 1; i < mNumbersCount; ++i) {
+ formatter.format(", '%s'", mNumbers[i]);
+ }
+
+ formatter.format(") AND ");
+ }
+
+ if (mDelivery == null || mDelivery.isEmpty()) {
+ formatter.format("type IN ('%d', '%d') AND ", kSmsTypeSentbox, kSmsTypeInbox);
+ } else if (mDelivery.equals("sent")) {
+ formatter.format("type = '%d' AND ", kSmsTypeSentbox);
+ } else if (mDelivery.equals("received")) {
+ formatter.format("type = '%d' AND ", kSmsTypeInbox);
+ } else {
+ throw new Exception("Unexpected delivery state: " + mDelivery);
+ }
+
+ if (mHasThreadId) {
+ formatter.format("thread_id = '%d' AND ", mThreadId);
+ }
+
+ // Final 'AND 1' is a no-op so we don't have to special case the last
+ // condition.
+ formatter.format("1");
+
+ ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+ cursor = cr.query(kSmsContentUri,
+ kRequiredMessageRows,
+ restrictions.toString(),
+ null,
+ mReverse ? "date DESC" : "date ASC");
+
+ if (cursor.getCount() == 0) {
+ notifyCursorDone(mRequestId);
+ return;
+ }
+
+ MessagesListManager.getInstance().add(mRequestId, cursor);
+
+ cursor.moveToFirst();
+ getMessageFromCursorAndNotify(cursor, mRequestId);
+ closeCursor = false;
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Error while trying to create a message list cursor", e);
+ notifyCursorError(kUnknownError, mRequestId);
+ } finally {
+ if (closeCursor && cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ if (!SmsIOThread.getInstance().execute(new CreateMessageCursorRunnable(aStartDate, aEndDate, aNumbers, aNumbersCount, aDelivery, aHasRead, aRead, aHasThreadId, aThreadId, aReverse, aRequestId))) {
+ Log.e("GeckoSmsManager", "Failed to add CreateMessageCursorRunnable to the SmsIOThread");
+ notifyCursorError(kUnknownError, aRequestId);
+ }
+ }
+
+ @Override
+ public void getNextMessage(int aRequestId) {
+ class GetNextMessageRunnable implements Runnable {
+ private final int mRequestId;
+
+ GetNextMessageRunnable(int aRequestId) {
+ mRequestId = aRequestId;
+ }
+
+ @Override
+ public void run() {
+ Cursor cursor = null;
+ boolean closeCursor = true;
+ try {
+ cursor = MessagesListManager.getInstance().get(mRequestId);
+
+ if (!cursor.moveToNext()) {
+ MessagesListManager.getInstance().remove(mRequestId);
+ notifyCursorDone(mRequestId);
+ return;
+ }
+
+ getMessageFromCursorAndNotify(cursor, mRequestId);
+ closeCursor = false;
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Error while trying to get the next message of a list", e);
+ notifyCursorError(kUnknownError, mRequestId);
+ } finally {
+ if (closeCursor) {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ if (!SmsIOThread.getInstance().execute(new GetNextMessageRunnable(aRequestId))) {
+ Log.e("GeckoSmsManager", "Failed to add GetNextMessageRunnable to the SmsIOThread");
+ notifyCursorError(kUnknownError, aRequestId);
+ }
+ }
+
+ private void getThreadFromCursorAndNotify(Cursor aCursor, int aRequestId) throws Exception {
+ ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+
+ long id = aCursor.getLong(aCursor.getColumnIndex("thread_id"));
+ Cursor msgCursor = cr.query(kSmsContentUri,
+ kRequiredMessageRowsForThread,
+ "thread_id = " + id,
+ null,
+ "date DESC");
+
+ if (msgCursor == null || msgCursor.getCount() == 0) {
+ throw new Exception("Empty thread " + id);
+ }
+
+ msgCursor.moveToFirst();
+
+ String lastMessageSubject = msgCursor.getString(msgCursor.getColumnIndex("subject"));
+ String body = msgCursor.getString(msgCursor.getColumnIndex("body"));
+ long timestamp = msgCursor.getLong(msgCursor.getColumnIndex("date"));
+
+ HashSet<String> participants = new HashSet<>();
+ do {
+ String p = msgCursor.getString(msgCursor.getColumnIndex("address"));
+ participants.add(p);
+ } while (msgCursor.moveToNext());
+
+ //TODO: handle MMS
+ String lastMessageType = "sms";
+
+ msgCursor = cr.query(kSmsContentUri,
+ kRequiredMessageRowsForThread,
+ "thread_id = " + id + " AND read = 0",
+ null,
+ null);
+
+ if (msgCursor == null) {
+ Log.e("GeckoSmsManager", "We should never get here, should have errored before");
+ throw new Exception("Empty thread " + id);
+ }
+
+ long unreadCount = msgCursor.getCount();
+
+ notifyThreadCursorResult(id, lastMessageSubject, body, unreadCount, participants.toArray(), timestamp, lastMessageType, aRequestId);
+ }
+
+ @Override
+ public void createThreadCursor(int aRequestId) {
+ class CreateThreadCursorRunnable implements Runnable {
+ private final int mRequestId;
+
+ CreateThreadCursorRunnable(int aRequestId) {
+ mRequestId = aRequestId;
+ }
+
+ @Override
+ public void run() {
+ try {
+ ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
+ Cursor cursor = cr.query(kSmsThreadsContentUri,
+ kThreadProjection,
+ null,
+ null,
+ "date DESC");
+ if (cursor == null || !cursor.moveToFirst()) {
+ notifyCursorDone(mRequestId);
+ return;
+ }
+
+ MessagesListManager.getInstance().add(mRequestId, cursor);
+
+ getThreadFromCursorAndNotify(cursor, mRequestId);
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Error while trying to create thread cursor: " + e);
+ notifyCursorError(kUnknownError, mRequestId);
+ }
+ }
+ }
+
+ if (!SmsIOThread.getInstance().execute(new CreateThreadCursorRunnable(aRequestId))) {
+ Log.e("GeckoSmsManager", "Failed to add CreateThreadCursorRunnable to the SmsIOThread");
+ notifyCursorError(kUnknownError, aRequestId);
+ }
+ }
+
+ @Override
+ public void getNextThread(int aRequestId) {
+ class GetNextThreadRunnable implements Runnable {
+ private final int mRequestId;
+
+ GetNextThreadRunnable(int aRequestId) {
+ mRequestId = aRequestId;
+ }
+
+ @Override
+ public void run() {
+ try {
+ Cursor cursor = MessagesListManager.getInstance().get(mRequestId);
+
+ if (!cursor.moveToNext()) {
+ MessagesListManager.getInstance().remove(mRequestId);
+ notifyCursorDone(mRequestId);
+ return;
+ }
+
+ getThreadFromCursorAndNotify(cursor, mRequestId);
+ } catch (Exception e) {
+ Log.e("GeckoSmsManager", "Error while trying to create thread cursor: " + e);
+ notifyCursorError(kUnknownError, mRequestId);
+ }
+ }
+ }
+
+ if (!SmsIOThread.getInstance().execute(new GetNextThreadRunnable(aRequestId))) {
+ Log.e("GeckoSmsManager", "Failed to add GetNextThreadRunnable to the SmsIOThread");
+ notifyCursorError(kUnknownError, aRequestId);
+ }
+ }
+
+ @Override
+ public void stop() {
+ GeckoAppShell.getContext().unregisterReceiver(this);
+ }
+
+ @Override
+ public void shutdown() {
+ SmsIOThread.getInstance().interrupt();
+ MessagesListManager.getInstance().clear();
+ }
+
+ private int getGeckoDeliveryStatus(int aDeliveryStatus) {
+ if (aDeliveryStatus == Telephony.Sms.STATUS_NONE) {
+ return kDeliveryStatusNotApplicable;
+ }
+ if (aDeliveryStatus >= Telephony.Sms.STATUS_FAILED) {
+ return kDeliveryStatusError;
+ }
+ if (aDeliveryStatus >= Telephony.Sms.STATUS_PENDING) {
+ return kDeliveryStatusPending;
+ }
+ return kDeliveryStatusSuccess;
+ }
+
+ private int getGeckoMessageClass(MessageClass aMessageClass) {
+ switch (aMessageClass) {
+ case CLASS_0:
+ return kMessageClassClass0;
+ case CLASS_1:
+ return kMessageClassClass1;
+ case CLASS_2:
+ return kMessageClassClass2;
+ case CLASS_3:
+ return kMessageClassClass3;
+ default:
+ return kMessageClassNormal;
+ }
+ }
+
+ static class IdTooHighException extends Exception {
+ private static final long serialVersionUID = 29935575131092050L;
+ }
+
+ static class InvalidTypeException extends Exception {
+ private static final long serialVersionUID = 47436856832535912L;
+ }
+
+ static class NotFoundException extends Exception {
+ private static final long serialVersionUID = 1940676816633984L;
+ }
+
+ static class TooManyResultsException extends Exception {
+ private static final long serialVersionUID = 51883196784325305L;
+ }
+
+ static class UnmatchingIdException extends Exception {
+ private static final long serialVersionUID = 158467542575633280L;
+ }
+
+ @WrapForJNI
+ public static native void notifySmsReceived(int aId, String aSender, String aBody, int aMessageClass, long aSentTimestamp, long aTimestamp);
+ @WrapForJNI
+ private static native void notifySmsSent(int aId, String aReceiver, String aBody, long aTimestamp, int aRequestId);
+ @WrapForJNI
+ private static native void notifySmsDelivery(int aId, int aDeliveryStatus, String aReceiver, String aBody, long aTimestamp);
+ @WrapForJNI
+ private static native void notifySmsSendFailed(int aError, int aRequestId);
+ @WrapForJNI
+ private static native void notifyGetSms(int aId, int aDeliveryStatus, String aReceiver, String aSender, String aBody, long aTimestamp, boolean aRead, int aRequestId);
+ @WrapForJNI
+ private static native void notifyGetSmsFailed(int aError, int aRequestId);
+ @WrapForJNI
+ private static native void notifySmsDeleted(boolean aDeleted, int aRequestId);
+ @WrapForJNI
+ private static native void notifySmsDeleteFailed(int aError, int aRequestId);
+ @WrapForJNI
+ private static native void notifySmsMarkedAsRead(boolean aMarkedAsRead, int aRequestId);
+ @WrapForJNI
+ private static native void notifySmsMarkAsReadFailed(int aError, int aRequestId);
+ @WrapForJNI
+ private static native void notifyCursorError(int aError, int aRequestId);
+ @WrapForJNI
+ private static native void notifyThreadCursorResult(long aId, String aLastMessageSubject, String aBody, long aUnreadCount, Object[] aParticipants, long aTimestamp, String aLastMessageType, int aRequestId);
+ @WrapForJNI
+ private static native void notifyMessageCursorResult(int aMessageId, int aDeliveryStatus, String aReceiver, String aSender, String aBody, long aTimestamp, long aThreadId, boolean aRead, int aRequestId);
+ @WrapForJNI
+ private static native void notifyCursorDone(int aRequestId);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
@@ -0,0 +1,680 @@
+/* -*- 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.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class GeckoThread extends Thread {
+ private static final String LOGTAG = "GeckoThread";
+
+ public enum State {
+ // After being loaded by class loader.
+ @WrapForJNI INITIAL(0),
+ // After launching Gecko thread
+ @WrapForJNI LAUNCHED(1),
+ // After loading the mozglue library.
+ @WrapForJNI MOZGLUE_READY(2),
+ // After loading the libxul library.
+ @WrapForJNI LIBS_READY(3),
+ // After initializing nsAppShell and JNI calls.
+ @WrapForJNI JNI_READY(4),
+ // After initializing profile and prefs.
+ @WrapForJNI PROFILE_READY(5),
+ // After initializing frontend JS
+ @WrapForJNI RUNNING(6),
+ // After leaving Gecko event loop
+ @WrapForJNI EXITING(3),
+ // After exiting GeckoThread (corresponding to "Gecko:Exited" event)
+ @WrapForJNI EXITED(0);
+
+ /* The rank is an arbitrary value reflecting the amount of components or features
+ * that are available for use. During startup and up to the RUNNING state, the
+ * rank value increases because more components are initialized and available for
+ * use. During shutdown and up to the EXITED state, the rank value decreases as
+ * components are shut down and become unavailable. EXITING has the same rank as
+ * LIBS_READY because both states have a similar amount of components available.
+ */
+ private final int rank;
+
+ private State(int rank) {
+ this.rank = rank;
+ }
+
+ public boolean is(final State other) {
+ return this == other;
+ }
+
+ public boolean isAtLeast(final State other) {
+ return this.rank >= other.rank;
+ }
+
+ public boolean isAtMost(final State other) {
+ return this.rank <= other.rank;
+ }
+
+ // Inclusive
+ public boolean isBetween(final State min, final State max) {
+ return this.rank >= min.rank && this.rank <= max.rank;
+ }
+ }
+
+ public static final State MIN_STATE = State.INITIAL;
+ public static final State MAX_STATE = State.EXITED;
+
+ private static volatile State sState = State.INITIAL;
+
+ private static class QueuedCall {
+ public Method method;
+ public Object target;
+ public Object[] args;
+ public State state;
+
+ public QueuedCall(final Method method, final Object target,
+ final Object[] args, final State state) {
+ this.method = method;
+ this.target = target;
+ this.args = args;
+ this.state = state;
+ }
+ }
+
+ private static final int QUEUED_CALLS_COUNT = 16;
+ private static final ArrayList<QueuedCall> QUEUED_CALLS = new ArrayList<>(QUEUED_CALLS_COUNT);
+
+ private static GeckoThread sGeckoThread;
+
+ @WrapForJNI
+ private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader();
+ @WrapForJNI
+ private static MessageQueue msgQueue;
+
+ private GeckoProfile mProfile;
+
+ private final String mArgs;
+ private final String mAction;
+ private final boolean mDebugging;
+
+ GeckoThread(GeckoProfile profile, String args, String action, boolean debugging) {
+ mProfile = profile;
+ mArgs = args;
+ mAction = action;
+ mDebugging = debugging;
+
+ setName("Gecko");
+ }
+
+ public static boolean init(GeckoProfile profile, String args, String action, boolean debugging) {
+ ThreadUtils.assertOnUiThread();
+ if (isState(State.INITIAL) && sGeckoThread == null) {
+ sGeckoThread = new GeckoThread(profile, args, action, debugging);
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean canUseProfile(final Context context, final GeckoProfile profile,
+ final String profileName, final File profileDir) {
+ if (profileDir != null && !profileDir.isDirectory()) {
+ return false;
+ }
+
+ if (profile == null) {
+ // We haven't initialized; any profile is okay as long as we follow the guest mode setting.
+ return GeckoProfile.shouldUse(context) ==
+ GeckoProfile.isGuestProfile(context, profileName, profileDir);
+ }
+
+ // We already initialized and have a profile; see if it matches ours.
+ try {
+ return profileDir == null ? profileName.equals(profile.getName()) :
+ profile.getDir().getCanonicalPath().equals(profileDir.getCanonicalPath());
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Cannot compare profile " + profileName);
+ return false;
+ }
+ }
+
+ public static boolean canUseProfile(final String profileName, final File profileDir) {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Null profile name");
+ }
+ return canUseProfile(GeckoAppShell.getApplicationContext(), getActiveProfile(),
+ profileName, profileDir);
+ }
+
+ public static boolean initWithProfile(final String profileName, final File profileDir) {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Null profile name");
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+ final GeckoProfile profile = getActiveProfile();
+
+ if (!canUseProfile(context, profile, profileName, profileDir)) {
+ // Profile is incompatible with current profile.
+ return false;
+ }
+
+ if (profile != null) {
+ // We already have a compatible profile.
+ return true;
+ }
+
+ // We haven't initialized yet; okay to initialize now.
+ return init(GeckoProfile.get(context, profileName, profileDir),
+ /* args */ null, /* action */ null, /* debugging */ false);
+ }
+
+ public static boolean launch() {
+ ThreadUtils.assertOnUiThread();
+ if (checkAndSetState(State.INITIAL, State.LAUNCHED)) {
+ sGeckoThread.start();
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isLaunched() {
+ return !isState(State.INITIAL);
+ }
+
+ @RobocopTarget
+ public static boolean isRunning() {
+ return isState(State.RUNNING);
+ }
+
+ // Invoke the given Method and handle checked Exceptions.
+ private static void invokeMethod(final Method method, final Object obj, final Object[] args) {
+ try {
+ method.setAccessible(true);
+ method.invoke(obj, args);
+ } catch (final IllegalAccessException e) {
+ throw new IllegalStateException("Unexpected exception", e);
+ } catch (final InvocationTargetException e) {
+ throw new UnsupportedOperationException("Cannot make call", e.getCause());
+ }
+ }
+
+ // Queue a call to the given method.
+ private static void queueNativeCallLocked(final Class<?> cls, final String methodName,
+ final Object obj, final Object[] args,
+ final State state) {
+ final ArrayList<Class<?>> argTypes = new ArrayList<>(args.length);
+ final ArrayList<Object> argValues = new ArrayList<>(args.length);
+
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] instanceof Class) {
+ argTypes.add((Class<?>) args[i]);
+ argValues.add(args[++i]);
+ continue;
+ }
+ Class<?> argType = args[i].getClass();
+ if (argType == Boolean.class) argType = Boolean.TYPE;
+ else if (argType == Byte.class) argType = Byte.TYPE;
+ else if (argType == Character.class) argType = Character.TYPE;
+ else if (argType == Double.class) argType = Double.TYPE;
+ else if (argType == Float.class) argType = Float.TYPE;
+ else if (argType == Integer.class) argType = Integer.TYPE;
+ else if (argType == Long.class) argType = Long.TYPE;
+ else if (argType == Short.class) argType = Short.TYPE;
+ argTypes.add(argType);
+ argValues.add(args[i]);
+ }
+ final Method method;
+ try {
+ method = cls.getDeclaredMethod(
+ methodName, argTypes.toArray(new Class<?>[argTypes.size()]));
+ } catch (final NoSuchMethodException e) {
+ throw new IllegalArgumentException("Cannot find method", e);
+ }
+
+ if (!Modifier.isNative(method.getModifiers())) {
+ // As a precaution, we disallow queuing non-native methods. Queuing non-native
+ // methods is dangerous because the method could end up being called on either
+ // the original thread or the Gecko thread depending on timing. Native methods
+ // usually handle this by posting an event to the Gecko thread automatically,
+ // but there is no automatic mechanism for non-native methods.
+ throw new UnsupportedOperationException("Not allowed to queue non-native methods");
+ }
+
+ if (isStateAtLeast(state)) {
+ invokeMethod(method, obj, argValues.toArray());
+ return;
+ }
+
+ QUEUED_CALLS.add(new QueuedCall(
+ method, obj, argValues.toArray(), state));
+ }
+
+ /**
+ * Queue a call to the given static method until Gecko is in the given state.
+ *
+ * @param state The Gecko state in which the native call could be executed.
+ * Default is State.RUNNING, which means this queued call will
+ * run when Gecko is at or after RUNNING state.
+ * @param cls Class that declares the static method.
+ * @param methodName Name of the static method.
+ * @param args Args to call the static method with; to specify a parameter type,
+ * pass in a Class instance first, followed by the value.
+ */
+ public static void queueNativeCallUntil(final State state, final Class<?> cls,
+ final String methodName, final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(cls, methodName, null, args, state);
+ }
+ }
+
+ /**
+ * Queue a call to the given static method until Gecko is in the RUNNING state.
+ */
+ public static void queueNativeCall(final Class<?> cls, final String methodName,
+ final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(cls, methodName, null, args, State.RUNNING);
+ }
+ }
+
+ /**
+ * Queue a call to the given instance method until Gecko is in the given state.
+ *
+ * @param state The Gecko state in which the native call could be executed.
+ * @param obj Object that declares the instance method.
+ * @param methodName Name of the instance method.
+ * @param args Args to call the instance method with; to specify a parameter type,
+ * pass in a Class instance first, followed by the value.
+ */
+ public static void queueNativeCallUntil(final State state, final Object obj,
+ final String methodName, final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, state);
+ }
+ }
+
+ /**
+ * Queue a call to the given instance method until Gecko is in the RUNNING state.
+ */
+ public static void queueNativeCall(final Object obj, final String methodName,
+ final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, State.RUNNING);
+ }
+ }
+
+ // Run all queued methods
+ private static void flushQueuedNativeCallsLocked(final State state) {
+ int lastSkipped = -1;
+ for (int i = 0; i < QUEUED_CALLS.size(); i++) {
+ final QueuedCall call = QUEUED_CALLS.get(i);
+ if (call == null) {
+ // We already handled the call.
+ continue;
+ }
+ if (!state.isAtLeast(call.state)) {
+ // The call is not ready yet; skip it.
+ lastSkipped = i;
+ continue;
+ }
+ // Mark as handled.
+ QUEUED_CALLS.set(i, null);
+
+ if (call.method == null) {
+ final GeckoEvent e = (GeckoEvent) call.target;
+ GeckoAppShell.notifyGeckoOfEvent(e);
+ e.recycle();
+ continue;
+ }
+ invokeMethod(call.method, call.target, call.args);
+ }
+ if (lastSkipped < 0) {
+ // We're done here; release the memory
+ QUEUED_CALLS.clear();
+ QUEUED_CALLS.trimToSize();
+ } else if (lastSkipped < QUEUED_CALLS.size() - 1) {
+ // We skipped some; free up null entries at the end,
+ // but keep all the previous entries for later.
+ QUEUED_CALLS.subList(lastSkipped + 1, QUEUED_CALLS.size()).clear();
+ }
+ }
+
+ private static String initGeckoEnvironment() {
+ final Context context = GeckoAppShell.getApplicationContext();
+ GeckoLoader.loadMozGlue(context);
+ setState(State.MOZGLUE_READY);
+
+ final Locale locale = Locale.getDefault();
+ final Resources res = context.getResources();
+ if (locale.toString().equalsIgnoreCase("zh_hk")) {
+ final Locale mappedLocale = Locale.TRADITIONAL_CHINESE;
+ Locale.setDefault(mappedLocale);
+ Configuration config = res.getConfiguration();
+ config.locale = mappedLocale;
+ res.updateConfiguration(config, null);
+ }
+
+ String[] pluginDirs = null;
+ try {
+ pluginDirs = GeckoAppShell.getPluginDirectories();
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Caught exception getting plugin dirs.", e);
+ }
+
+ final String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.setupGeckoEnvironment(context, pluginDirs, context.getFilesDir().getPath());
+
+ GeckoLoader.loadSQLiteLibs(context, resourcePath);
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+ GeckoLoader.loadGeckoLibs(context, resourcePath);
+ setState(State.LIBS_READY);
+
+ return resourcePath;
+ }
+
+ private static String getTypeFromAction(String action) {
+ if (GeckoAppShell.ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
+ return "-bookmark";
+ }
+ return null;
+ }
+
+ private String addCustomProfileArg(String args) {
+ String profileArg = "";
+
+ // Make sure a profile exists.
+ final GeckoProfile profile = getProfile();
+ profile.getDir(); // call the lazy initializer
+
+ // If args don't include the profile, make sure it's included.
+ if (args == null || !args.matches(".*\\B-(P|profile)\\s+\\S+.*")) {
+ if (profile.isCustomProfile()) {
+ profileArg = " -profile " + profile.getDir().getAbsolutePath();
+ } else {
+ profileArg = " -P " + profile.getName();
+ }
+ }
+
+ return (args != null ? args : "") + profileArg;
+ }
+
+ private String getGeckoArgs(final String apkPath) {
+ // argv[0] is the program name, which for us is the package name.
+ final Context context = GeckoAppShell.getApplicationContext();
+ final StringBuilder args = new StringBuilder(context.getPackageName());
+ args.append(" -greomni ").append(apkPath);
+
+ final String userArgs = addCustomProfileArg(mArgs);
+ if (userArgs != null) {
+ args.append(' ').append(userArgs);
+ }
+
+ final String type = getTypeFromAction(mAction);
+ if (type != null) {
+ args.append(" ").append(type);
+ }
+
+ // In un-official builds, we want to load Javascript resources fresh
+ // with each build. In official builds, the startup cache is purged by
+ // the buildid mechanism, but most un-official builds don't bump the
+ // buildid, so we purge here instead.
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Log.w(LOGTAG, "STARTUP PERFORMANCE WARNING: un-official build: purging the " +
+ "startup (JavaScript) caches.");
+ args.append(" -purgecaches");
+ }
+
+ return args.toString();
+ }
+
+ public static GeckoProfile getActiveProfile() {
+ if (sGeckoThread == null) {
+ return null;
+ }
+ final GeckoProfile profile = sGeckoThread.mProfile;
+ if (profile != null) {
+ return profile;
+ }
+ return sGeckoThread.getProfile();
+ }
+
+ public synchronized GeckoProfile getProfile() {
+ if (mProfile == null) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ mProfile = GeckoProfile.initFromArgs(context, mArgs);
+ }
+ return mProfile;
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ GeckoThread.msgQueue = Looper.myQueue();
+ ThreadUtils.sGeckoThread = this;
+ ThreadUtils.sGeckoHandler = new Handler();
+
+ // Preparation for pumpMessageLoop()
+ final MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
+ @Override public boolean queueIdle() {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+ Message idleMsg = Message.obtain(geckoHandler);
+ // Use |Message.obj == GeckoHandler| to identify our "queue is empty" message
+ idleMsg.obj = geckoHandler;
+ geckoHandler.sendMessageAtFrontOfQueue(idleMsg);
+ // Keep this IdleHandler
+ return true;
+ }
+ };
+ Looper.myQueue().addIdleHandler(idleHandler);
+
+ if (mDebugging) {
+ try {
+ Thread.sleep(5 * 1000 /* 5 seconds */);
+ } catch (final InterruptedException e) {
+ }
+ }
+
+ final String args = getGeckoArgs(initGeckoEnvironment());
+
+ // This can only happen after the call to initGeckoEnvironment
+ // above, because otherwise the JNI code hasn't been loaded yet.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override public void run() {
+ GeckoAppShell.registerJavaUiThread();
+ }
+ });
+
+ Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - runGecko");
+
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Log.i(LOGTAG, "RunGecko - args = " + args);
+ }
+
+ // And go.
+ GeckoLoader.nativeRun(args);
+
+ // And... we're done.
+ setState(State.EXITED);
+
+ try {
+ final JSONObject msg = new JSONObject();
+ msg.put("type", "Gecko:Exited");
+ EventDispatcher.getInstance().dispatchEvent(msg, null);
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "unable to dispatch event", e);
+ }
+
+ // Remove pumpMessageLoop() idle handler
+ Looper.myQueue().removeIdleHandler(idleHandler);
+ }
+
+ public static void addPendingEvent(final GeckoEvent e) {
+ synchronized (QUEUED_CALLS) {
+ if (isRunning()) {
+ // We may just have switched to running state.
+ GeckoAppShell.notifyGeckoOfEvent(e);
+ e.recycle();
+ } else {
+ QUEUED_CALLS.add(new QueuedCall(null, e, null, State.RUNNING));
+ }
+ }
+ }
+
+ @WrapForJNI
+ private static boolean pumpMessageLoop(final Message msg) {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+
+ if (msg.obj == geckoHandler && msg.getTarget() == geckoHandler) {
+ // Our "queue is empty" message; see runGecko()
+ return false;
+ }
+
+ if (msg.getTarget() == null) {
+ Looper.myLooper().quit();
+ } else {
+ msg.getTarget().dispatchMessage(msg);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check that the current Gecko thread state matches the given state.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isState(final State state) {
+ return sState.is(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or further along,
+ * according to the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtLeast(final State state) {
+ return sState.isAtLeast(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or prior,
+ * according to the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtMost(final State state) {
+ return sState.isAtMost(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state falls into an inclusive range of states,
+ * according to the order defined in the State enum.
+ *
+ * @param minState Lower range of allowable states
+ * @param maxState Upper range of allowable states
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateBetween(final State minState, final State maxState) {
+ return sState.isBetween(minState, maxState);
+ }
+
+ @WrapForJNI
+ private static void setState(final State newState) {
+ ThreadUtils.assertOnGeckoThread();
+ synchronized (QUEUED_CALLS) {
+ flushQueuedNativeCallsLocked(newState);
+ sState = newState;
+ }
+ }
+
+ @WrapForJNI
+ private static boolean checkAndSetState(final State currentState, final State newState) {
+ synchronized (QUEUED_CALLS) {
+ if (sState == currentState) {
+ flushQueuedNativeCallsLocked(newState);
+ sState = newState;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @WrapForJNI(stubName = "SpeculativeConnect")
+ private static native void speculativeConnectNative(String uri);
+
+ public static void speculativeConnect(final String uri) {
+ // This is almost always called before Gecko loads, so we don't
+ // bother checking here if Gecko is actually loaded or not.
+ // Speculative connection depends on proxy settings,
+ // so the earliest it can happen is after profile is ready.
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "speculativeConnectNative", uri);
+ }
+
+ @WrapForJNI @RobocopTarget
+ public static native void waitOnGecko();
+
+ @WrapForJNI(stubName = "OnPause")
+ private static native void nativeOnPause();
+
+ public static void onPause() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnPause();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "nativeOnPause");
+ }
+ }
+
+ @WrapForJNI(stubName = "OnResume")
+ private static native void nativeOnResume();
+
+ public static void onResume() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnResume();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "nativeOnResume");
+ }
+ }
+
+ @WrapForJNI(stubName = "CreateServices")
+ private static native void nativeCreateServices(String category, String data);
+
+ public static void createServices(final String category, final String data) {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeCreateServices(category, data);
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class, "nativeCreateServices",
+ String.class, category, String.class, data);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
@@ -0,0 +1,713 @@
+/* -*- 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.Set;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.GLController;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+public class GeckoView extends LayerView
+ implements ContextGetter {
+
+ private static final String DEFAULT_SHARED_PREFERENCES_FILE = "GeckoView";
+ private static final String LOGTAG = "GeckoView";
+
+ private ChromeDelegate mChromeDelegate;
+ private ContentDelegate mContentDelegate;
+
+ private InputConnectionListener mInputConnectionListener;
+
+ private final GeckoEventListener mGeckoEventListener = new GeckoEventListener() {
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (event.equals("Gecko:Ready")) {
+ handleReady(message);
+ } else if (event.equals("Content:StateChange")) {
+ handleStateChange(message);
+ } else if (event.equals("Content:LoadError")) {
+ handleLoadError(message);
+ } else if (event.equals("Content:PageShow")) {
+ handlePageShow(message);
+ } else if (event.equals("DOMTitleChanged")) {
+ handleTitleChanged(message);
+ } else if (event.equals("Link:Favicon")) {
+ handleLinkFavicon(message);
+ } else if (event.equals("Prompt:Show") || event.equals("Prompt:ShowTop")) {
+ handlePrompt(message);
+ } else if (event.equals("Accessibility:Event")) {
+ int mode = getImportantForAccessibility();
+ if (mode == View.IMPORTANT_FOR_ACCESSIBILITY_YES ||
+ mode == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ GeckoAccessibility.sendAccessibilityEvent(message);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "handleMessage threw for " + event, e);
+ }
+ }
+ });
+ }
+ };
+
+ private final NativeEventListener mNativeEventListener = new NativeEventListener() {
+ @Override
+ public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
+ try {
+ if ("Accessibility:Ready".equals(event)) {
+ GeckoAccessibility.updateAccessibilitySettings(getContext());
+ } else if ("GeckoView:Message".equals(event)) {
+ // We need to pull out the bundle while on the Gecko thread.
+ NativeJSObject json = message.optObject("data", null);
+ if (json == null) {
+ // Must have payload to call the message handler.
+ return;
+ }
+ final Bundle data = json.toBundle();
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleScriptMessage(data, callback);
+ }
+ });
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "handleMessage threw for " + event, e);
+ }
+ }
+ };
+
+ @WrapForJNI
+ private static final class Window extends JNIObject {
+ /* package */ final GLController glController = new GLController();
+
+ static native void open(Window instance, GeckoView view, GLController glController,
+ String chromeURI,
+ int width, int height);
+
+ @Override protected native void disposeNative();
+ native void close();
+ native void reattach(GeckoView view);
+ }
+
+ // Object to hold onto our nsWindow connection when GeckoView gets destroyed.
+ private static class StateBinder extends Binder implements Parcelable {
+ public final Parcelable superState;
+ public final Window window;
+
+ public StateBinder(Parcelable superState, Window window) {
+ this.superState = superState;
+ this.window = window;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ // Always write out the super-state, so that even if we lose this binder, we
+ // will still have something to pass into super.onRestoreInstanceState.
+ out.writeParcelable(superState, flags);
+ out.writeStrongBinder(this);
+ }
+
+ @ReflectionTarget
+ public static final Parcelable.Creator<StateBinder> CREATOR
+ = new Parcelable.Creator<StateBinder>() {
+ @Override
+ public StateBinder createFromParcel(Parcel in) {
+ final Parcelable superState = in.readParcelable(null);
+ final IBinder binder = in.readStrongBinder();
+ if (binder instanceof StateBinder) {
+ return (StateBinder) binder;
+ }
+ // Not the original object we saved; return null state.
+ return new StateBinder(superState, null);
+ }
+
+ @Override
+ public StateBinder[] newArray(int size) {
+ return new StateBinder[size];
+ }
+ };
+ }
+
+ private Window window;
+ private boolean stateSaved;
+
+ public GeckoView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public GeckoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ if (GeckoAppShell.getApplicationContext() == null) {
+ GeckoAppShell.setApplicationContext(context.getApplicationContext());
+ }
+
+ // Set the GeckoInterface if the context is an activity and the GeckoInterface
+ // has not already been set
+ if (context instanceof Activity && getGeckoInterface() == null) {
+ setGeckoInterface(new BaseGeckoInterface(context));
+ GeckoAppShell.setContextGetter(this);
+ }
+
+ // Perform common initialization for Fennec/GeckoView.
+ GeckoAppShell.setLayerView(this);
+
+ initializeView(EventDispatcher.getInstance());
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState()
+ {
+ final Parcelable superState = super.onSaveInstanceState();
+ stateSaved = true;
+ return new StateBinder(superState, this.window);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state)
+ {
+ final StateBinder stateBinder = (StateBinder) state;
+ // We have to always call super.onRestoreInstanceState because View keeps
+ // track of these calls and throws an exception when we don't call it.
+ super.onRestoreInstanceState(stateBinder.superState);
+
+ if (stateBinder.window != null) {
+ this.window = stateBinder.window;
+ }
+ stateSaved = false;
+ }
+
+ @Override
+ public void onAttachedToWindow()
+ {
+ final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+
+ if (window == null) {
+ // Open a new nsWindow if we didn't have one from before.
+ window = new Window();
+ final String chromeURI = getGeckoInterface().getDefaultChromeURI();
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ Window.open(window, this, window.glController,
+ chromeURI,
+ metrics.widthPixels, metrics.heightPixels);
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, Window.class,
+ "open", window, GeckoView.class, this, window.glController,
+ chromeURI,
+ metrics.widthPixels, metrics.heightPixels);
+ }
+ } else {
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ window.reattach(this);
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "reattach", GeckoView.class, this);
+ }
+ }
+
+ setGLController(window.glController);
+ super.onAttachedToWindow();
+ }
+
+ @Override
+ public void onDetachedFromWindow()
+ {
+ super.onDetachedFromWindow();
+ super.destroy();
+
+ if (stateSaved) {
+ // If we saved state earlier, we don't want to close the nsWindow.
+ return;
+ }
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ window.close();
+ window.disposeNative();
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "close");
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "disposeNative");
+ }
+ }
+
+ /* package */ void setInputConnectionListener(final InputConnectionListener icl) {
+ mInputConnectionListener = icl;
+ }
+
+ @Override
+ public Handler getHandler() {
+ if (mInputConnectionListener != null) {
+ return mInputConnectionListener.getHandler(super.getHandler());
+ }
+ return super.getHandler();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ if (mInputConnectionListener != null) {
+ return mInputConnectionListener.onCreateInputConnection(outAttrs);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (super.onKeyPreIme(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (super.onKeyUp(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (super.onKeyDown(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ if (super.onKeyLongPress(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyLongPress(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ if (super.onKeyMultiple(keyCode, repeatCount, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ /* package */ boolean isIMEEnabled() {
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.isIMEEnabled();
+ }
+
+ public void importScript(final String url) {
+ if (url.startsWith("resource://android/assets/")) {
+ GeckoAppShell.notifyObservers("GeckoView:ImportScript", url);
+ return;
+ }
+
+ throw new IllegalArgumentException("Must import script from 'resources://android/assets/' location.");
+ }
+
+ private void connectToGecko() {
+ GeckoAppShell.notifyObservers("Viewport:Flush", null);
+ }
+
+ private void handleReady(final JSONObject message) {
+ connectToGecko();
+
+ if (mChromeDelegate != null) {
+ mChromeDelegate.onReady(this);
+ }
+ }
+
+ private void handleStateChange(final JSONObject message) throws JSONException {
+ int state = message.getInt("state");
+ if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
+ if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStart(this, new Browser(id), message.getString("uri"));
+ }
+ } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStop(this, new Browser(id), message.getBoolean("success"));
+ }
+ }
+ }
+ }
+
+ private void handleLoadError(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStop(GeckoView.this, new Browser(id), false);
+ }
+ }
+
+ private void handlePageShow(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageShow(GeckoView.this, new Browser(id));
+ }
+ }
+
+ private void handleTitleChanged(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onReceivedTitle(GeckoView.this, new Browser(id), message.getString("title"));
+ }
+ }
+
+ private void handleLinkFavicon(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onReceivedFavicon(GeckoView.this, new Browser(id), message.getString("href"), message.getInt("size"));
+ }
+ }
+
+ private void handlePrompt(final JSONObject message) throws JSONException {
+ if (mChromeDelegate != null) {
+ String hint = message.optString("hint");
+ if ("alert".equals(hint)) {
+ String text = message.optString("text");
+ mChromeDelegate.onAlert(GeckoView.this, null, text, new PromptResult(message));
+ } else if ("confirm".equals(hint)) {
+ String text = message.optString("text");
+ mChromeDelegate.onConfirm(GeckoView.this, null, text, new PromptResult(message));
+ } else if ("prompt".equals(hint)) {
+ String text = message.optString("text");
+ String defaultValue = message.optString("textbox0");
+ mChromeDelegate.onPrompt(GeckoView.this, null, text, defaultValue, new PromptResult(message));
+ } else if ("remotedebug".equals(hint)) {
+ mChromeDelegate.onDebugRequest(GeckoView.this, new PromptResult(message));
+ }
+ }
+ }
+
+ private void handleScriptMessage(final Bundle data, final EventCallback callback) {
+ if (mChromeDelegate != null) {
+ MessageResult result = null;
+ if (callback != null) {
+ result = new MessageResult(callback);
+ }
+ mChromeDelegate.onScriptMessage(GeckoView.this, data, result);
+ }
+ }
+
+ /**
+ * Set the chrome callback handler.
+ * This will replace the current handler.
+ * @param chrome An implementation of GeckoViewChrome.
+ */
+ public void setChromeDelegate(ChromeDelegate chrome) {
+ mChromeDelegate = chrome;
+ }
+
+ /**
+ * Set the content callback handler.
+ * This will replace the current handler.
+ * @param content An implementation of ContentDelegate.
+ */
+ public void setContentDelegate(ContentDelegate content) {
+ mContentDelegate = content;
+ }
+
+ public static void setGeckoInterface(final BaseGeckoInterface geckoInterface) {
+ GeckoAppShell.setGeckoInterface(geckoInterface);
+ }
+
+ public static GeckoAppShell.GeckoInterface getGeckoInterface() {
+ return GeckoAppShell.getGeckoInterface();
+ }
+
+ protected String getSharedPreferencesFile() {
+ return DEFAULT_SHARED_PREFERENCES_FILE;
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences() {
+ return getContext().getSharedPreferences(getSharedPreferencesFile(), 0);
+ }
+
+ /**
+ * Wrapper for a browser in the GeckoView container. Associated with a browser
+ * element in the Gecko system.
+ */
+ public class Browser {
+ private final int mId;
+ private Browser(int Id) {
+ mId = Id;
+ }
+
+ /**
+ * Get the ID of the Browser. This is the same ID used by Gecko for it's underlying
+ * browser element.
+ * @return The integer ID of the Browser.
+ */
+ private int getId() {
+ return mId;
+ }
+
+ /**
+ * Load a URL resource into the Browser.
+ * @param url The URL string.
+ */
+ public void loadUrl(String url) {
+ JSONObject args = new JSONObject();
+ try {
+ args.put("url", url);
+ args.put("parentId", -1);
+ args.put("newTab", false);
+ args.put("tabID", mId);
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
+ }
+ GeckoAppShell.notifyObservers("Tab:Load", args.toString());
+ }
+ }
+
+ /* Provides a means for the client to indicate whether a JavaScript
+ * dialog request should proceed. An instance of this class is passed to
+ * various GeckoViewChrome callback actions.
+ */
+ public class PromptResult {
+ private final int RESULT_OK = 0;
+ private final int RESULT_CANCEL = 1;
+
+ private final JSONObject mMessage;
+
+ public PromptResult(JSONObject message) {
+ mMessage = message;
+ }
+
+ private JSONObject makeResult(int resultCode) {
+ JSONObject result = new JSONObject();
+ try {
+ result.put("button", resultCode);
+ } catch (JSONException ex) { }
+ return result;
+ }
+
+ /**
+ * Handle a confirmation response from the user.
+ */
+ public void confirm() {
+ JSONObject result = makeResult(RESULT_OK);
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+
+ /**
+ * Handle a confirmation response from the user.
+ * @param value String value to return to the browser context.
+ */
+ public void confirmWithValue(String value) {
+ JSONObject result = makeResult(RESULT_OK);
+ try {
+ result.put("textbox0", value);
+ } catch (JSONException ex) { }
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+
+ /**
+ * Handle a cancellation response from the user.
+ */
+ public void cancel() {
+ JSONObject result = makeResult(RESULT_CANCEL);
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+ }
+
+ /* Provides a means for the client to respond to a script message with some data.
+ * An instance of this class is passed to GeckoViewChrome.onScriptMessage.
+ */
+ public class MessageResult {
+ private final EventCallback mCallback;
+
+ public MessageResult(EventCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("EventCallback should not be null.");
+ }
+ mCallback = callback;
+ }
+
+ private JSONObject bundleToJSON(Bundle data) {
+ JSONObject result = new JSONObject();
+ if (data == null) {
+ return result;
+ }
+
+ final Set<String> keys = data.keySet();
+ for (String key : keys) {
+ try {
+ result.put(key, data.get(key));
+ } catch (JSONException e) {
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Handle a successful response to a script message.
+ * @param value Bundle value to return to the script context.
+ */
+ public void success(Bundle data) {
+ mCallback.sendSuccess(bundleToJSON(data));
+ }
+
+ /**
+ * Handle a failure response to a script message.
+ */
+ public void failure(Bundle data) {
+ mCallback.sendError(bundleToJSON(data));
+ }
+ }
+
+ public interface ChromeDelegate {
+ /**
+ * Tell the host application that Gecko is ready to handle requests.
+ * @param view The GeckoView that initiated the callback.
+ */
+ public void onReady(GeckoView view);
+
+ /**
+ * Tell the host application to display an alert dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display a confirmation dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display an input prompt dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param defaultValue The string to use as default input.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display a remote debugging request dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onDebugRequest(GeckoView view, GeckoView.PromptResult result);
+
+ /**
+ * Receive a message from an imported script.
+ * @param view The GeckoView that initiated the callback.
+ * @param data Bundle of data sent with the message. Never null.
+ * @param result A MessageResult used to send back a response without blocking. Can be null.
+ * Defaults to do nothing.
+ */
+ public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result);
+ }
+
+ public interface ContentDelegate {
+ /**
+ * A Browser has started loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param url The resource being loaded.
+ */
+ public void onPageStart(GeckoView view, GeckoView.Browser browser, String url);
+
+ /**
+ * A Browser has finished loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that was loading the content.
+ * @param success Whether the page loaded successfully or an error occurred.
+ */
+ public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success);
+
+ /**
+ * A Browser is displaying content. This page could have been loaded via
+ * network or from the session history.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ */
+ public void onPageShow(GeckoView view, GeckoView.Browser browser);
+
+ /**
+ * A page title was discovered in the content or updated after the content
+ * loaded.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param title The title sent from the content.
+ */
+ public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title);
+
+ /**
+ * A link element was discovered in the content or updated after the content
+ * loaded that specifies a favicon.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param url The href of the link element specifying the favicon.
+ * @param size The maximum size specified for the favicon, or -1 for any size.
+ */
+ public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size);
+ }
+
+}
rename from mobile/android/base/java/org/mozilla/gecko/GeckoViewChrome.java
rename to mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java
rename from mobile/android/base/java/org/mozilla/gecko/GeckoViewContent.java
rename to mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java
@@ -0,0 +1,25 @@
+/* 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.Handler;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Interface for interacting with GeckoInputConnection from GeckoView.
+ */
+interface InputConnectionListener
+{
+ Handler getHandler(Handler defHandler);
+ InputConnection onCreateInputConnection(EditorInfo outAttrs);
+ boolean onKeyPreIme(int keyCode, KeyEvent event);
+ boolean onKeyDown(int keyCode, KeyEvent event);
+ boolean onKeyLongPress(int keyCode, KeyEvent event);
+ boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event);
+ boolean onKeyUp(int keyCode, KeyEvent event);
+ boolean isIMEEnabled();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java
@@ -0,0 +1,76 @@
+/* -*- 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.Collection;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.Context;
+import android.provider.Settings.Secure;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+
+final public class InputMethods {
+ public static final String METHOD_ANDROID_LATINIME = "com.android.inputmethod.latin/.LatinIME";
+ public static final String METHOD_ATOK = "com.justsystems.atokmobile.service/.AtokInputMethodService";
+ public static final String METHOD_GOOGLE_JAPANESE_INPUT = "com.google.android.inputmethod.japanese/.MozcService";
+ public static final String METHOD_GOOGLE_LATINIME = "com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME";
+ public static final String METHOD_HTC_TOUCH_INPUT = "com.htc.android.htcime/.HTCIMEService";
+ public static final String METHOD_IWNN = "jp.co.omronsoft.iwnnime.ml/.standardcommon.IWnnLanguageSwitcher";
+ public static final String METHOD_OPENWNN_PLUS = "com.owplus.ime.openwnnplus/.OpenWnnJAJP";
+ public static final String METHOD_SAMSUNG = "com.sec.android.inputmethod/.SamsungKeypad";
+ public static final String METHOD_SIMEJI = "com.adamrocker.android.input.simeji/.OpenWnnSimeji";
+ public static final String METHOD_SWIFTKEY = "com.touchtype.swiftkey/com.touchtype.KeyboardService";
+ public static final String METHOD_SWYPE = "com.swype.android.inputmethod/.SwypeInputMethod";
+ public static final String METHOD_SWYPE_BETA = "com.nuance.swype.input/.IME";
+ public static final String METHOD_TOUCHPAL_KEYBOARD = "com.cootek.smartinputv5/com.cootek.smartinput5.TouchPalIME";
+
+ private InputMethods() {}
+
+ public static String getCurrentInputMethod(Context context) {
+ String inputMethod = Secure.getString(context.getContentResolver(), Secure.DEFAULT_INPUT_METHOD);
+ return (inputMethod != null ? inputMethod : "");
+ }
+
+ public static InputMethodInfo getInputMethodInfo(Context context, String inputMethod) {
+ InputMethodManager imm = getInputMethodManager(context);
+ Collection<InputMethodInfo> infos = imm.getEnabledInputMethodList();
+ for (InputMethodInfo info : infos) {
+ if (info.getId().equals(inputMethod)) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ public static InputMethodManager getInputMethodManager(Context context) {
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+
+ public static boolean needsSoftResetWorkaround(String inputMethod) {
+ // Stock latin IME on Android 4.2 and above
+ return Versions.feature17Plus &&
+ (METHOD_ANDROID_LATINIME.equals(inputMethod) ||
+ METHOD_GOOGLE_LATINIME.equals(inputMethod));
+ }
+
+ public static boolean shouldCommitCharAsKey(String inputMethod) {
+ return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
+ }
+
+ public static boolean isGestureKeyboard(Context context) {
+ // SwiftKey is a gesture keyboard, but it doesn't seem to need any special-casing
+ // to do AwesomeBar auto-spacing.
+ String inputMethod = getCurrentInputMethod(context);
+ return (Versions.feature17Plus &&
+ (METHOD_ANDROID_LATINIME.equals(inputMethod) ||
+ METHOD_GOOGLE_LATINIME.equals(inputMethod))) ||
+ METHOD_SWYPE.equals(inputMethod) ||
+ METHOD_SWYPE_BETA.equals(inputMethod) ||
+ METHOD_TOUCHPAL_KEYBOARD.equals(inputMethod);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java
@@ -0,0 +1,55 @@
+/* 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.mozglue.GeckoLoader;
+
+import android.content.Context;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public class NSSBridge {
+ private static final String LOGTAG = "NSSBridge";
+
+ private static native String nativeEncrypt(String aDb, String aValue);
+ private static native String nativeDecrypt(String aDb, String aValue);
+
+ @RobocopTarget
+ static public String encrypt(Context context, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ String path = GeckoProfile.get(context).getDir().toString();
+ return nativeEncrypt(path, aValue);
+ }
+
+ @RobocopTarget
+ static public String encrypt(Context context, String profilePath, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ return nativeEncrypt(profilePath, aValue);
+ }
+
+ @RobocopTarget
+ static public String decrypt(Context context, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ String path = GeckoProfile.get(context).getDir().toString();
+ return nativeDecrypt(path, aValue);
+ }
+
+ @RobocopTarget
+ static public String decrypt(Context context, String profilePath, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ return nativeDecrypt(profilePath, aValue);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationClient.java
@@ -0,0 +1,212 @@
+/* -*- 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.Notification;
+import android.app.PendingIntent;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.LinkedList;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Client for posting notifications through a NotificationHandler.
+ */
+public abstract class NotificationClient {
+ private static final String LOGTAG = "GeckoNotificationClient";
+
+ private volatile NotificationHandler mHandler;
+ private boolean mReady;
+ private final LinkedList<Runnable> mTaskQueue = new LinkedList<Runnable>();
+ private final ConcurrentHashMap<Integer, UpdateRunnable> mUpdatesMap =
+ new ConcurrentHashMap<Integer, UpdateRunnable>();
+
+ /**
+ * Runnable that is reused between update notifications.
+ *
+ * Updates happen frequently, so reusing Runnables prevents frequent dynamic allocation.
+ */
+ private class UpdateRunnable implements Runnable {
+ private long mProgress;
+ private long mProgressMax;
+ private String mAlertText;
+ final private int mNotificationID;
+
+ public UpdateRunnable(int notificationID) {
+ mNotificationID = notificationID;
+ }
+
+ public synchronized boolean updateProgress(long progress, long progressMax, String alertText) {
+ if (progress == mProgress
+ && mProgressMax == progressMax
+ && TextUtils.equals(mAlertText, alertText)) {
+ return false;
+ }
+
+ mProgress = progress;
+ mProgressMax = progressMax;
+ mAlertText = alertText;
+ return true;
+ }
+
+ @Override
+ public void run() {
+ long progress;
+ long progressMax;
+ String alertText;
+
+ synchronized (this) {
+ progress = mProgress;
+ progressMax = mProgressMax;
+ alertText = mAlertText;
+ }
+
+ mHandler.update(mNotificationID, progress, progressMax, alertText);
+ }
+ };
+
+ /**
+ * Adds a notification.
+ *
+ * @see NotificationHandler#add(int, String, String, String, PendingIntent, PendingIntent)
+ */
+ public synchronized void add(final int notificationID, final String aImageUrl, final String aHost,
+ final String aAlertTitle, final String aAlertText, final PendingIntent contentIntent) {
+ mTaskQueue.add(new Runnable() {
+ @Override
+ public void run() {
+ mHandler.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
+ }
+ });
+ notify();
+
+ if (!mReady) {
+ bind();
+ }
+ }
+
+ /**
+ * Adds a notification.
+ *
+ * @see NotificationHandler#add(int, Notification)
+ */
+ public synchronized void add(final int notificationID, final Notification notification) {
+ mTaskQueue.add(new Runnable() {
+ @Override
+ public void run() {
+ mHandler.add(notificationID, notification);
+ }
+ });
+ notify();
+
+ if (!mReady) {
+ bind();
+ }
+ }
+
+ /**
+ * Updates a notification.
+ *
+ * @see NotificationHandler#update(int, long, long, String)
+ */
+ public void update(final int notificationID, final long aProgress, final long aProgressMax,
+ final String aAlertText) {
+ UpdateRunnable runnable = mUpdatesMap.get(notificationID);
+
+ if (runnable == null) {
+ runnable = new UpdateRunnable(notificationID);
+ mUpdatesMap.put(notificationID, runnable);
+ }
+
+ // If we've already posted an update with these values, there's no
+ // need to do it again.
+ if (!runnable.updateProgress(aProgress, aProgressMax, aAlertText)) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mReady) {
+ mTaskQueue.add(runnable);
+ notify();
+ }
+ }
+ }
+
+ /**
+ * Removes a notification.
+ *
+ * @see NotificationHandler#remove(int)
+ */
+ public synchronized void remove(final int notificationID) {
+ mTaskQueue.add(new Runnable() {
+ @Override
+ public void run() {
+ mHandler.remove(notificationID);
+ mUpdatesMap.remove(notificationID);
+ }
+ });
+
+ // If mReady == false, we haven't added any notifications yet. That can happen if Fennec is being
+ // started in response to clicking a notification. Call bind() to ensure the task we posted above is run.
+ if (!mReady) {
+ bind();
+ }
+
+ notify();
+ }
+
+ /**
+ * Determines whether a notification is showing progress.
+ *
+ * @see NotificationHandler#isProgressStyle(int)
+ */
+ public boolean isOngoing(int notificationID) {
+ final NotificationHandler handler = mHandler;
+ return handler != null && handler.isOngoing(notificationID);
+ }
+
+ protected void bind() {
+ mReady = true;
+ }
+
+ protected void unbind() {
+ mReady = false;
+ mUpdatesMap.clear();
+ }
+
+ protected void connectHandler(NotificationHandler handler) {
+ mHandler = handler;
+ new Thread(new NotificationRunnable()).start();
+ }
+
+ private class NotificationRunnable implements Runnable {
+ @Override
+ public void run() {
+ Runnable r;
+ try {
+ while (true) {
+ // Synchronize polls to prevent tasks from being added to the queue
+ // during the isDone check.
+ synchronized (NotificationClient.this) {
+ r = mTaskQueue.poll();
+ while (r == null) {
+ if (mHandler.isDone()) {
+ unbind();
+ return;
+ }
+ NotificationClient.this.wait();
+ r = mTaskQueue.poll();
+ }
+ }
+ r.run();
+ }
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "Notification task queue processing interrupted", e);
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationHandler.java
@@ -0,0 +1,193 @@
+/* -*- 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.graphics.Bitmap;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import org.mozilla.gecko.gfx.BitmapUtils;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+public class NotificationHandler {
+ private static String LOGTAG = "GeckoNotifHandler";
+ private final ConcurrentHashMap<Integer, Notification>
+ mNotifications = new ConcurrentHashMap<Integer, Notification>();
+ private final Context mContext;
+ private final NotificationManagerCompat mNotificationManager;
+
+ /**
+ * Notification associated with this service's foreground state.
+ *
+ * {@link android.app.Service#startForeground(int, android.app.Notification)}
+ * associates the foreground with exactly one notification from the service.
+ * To keep Fennec alive during downloads (and to make sure it can be killed
+ * once downloads are complete), we make sure that the foreground is always
+ * associated with an active progress notification if and only if at least
+ * one download is in progress.
+ */
+ private Notification mForegroundNotification;
+ private int mForegroundNotificationId;
+
+ public NotificationHandler(Context context) {
+ mContext = context;
+ mNotificationManager = NotificationManagerCompat.from(context);
+ }
+
+ /**
+ * Adds a notification.
+ *
+ * @param notificationID the unique ID of the notification
+ * @param aImageUrl URL of the image to use
+ * @param aAlertTitle title of the notification
+ * @param aAlertText text of the notification
+ * @param contentIntent Intent used when the notification is clicked
+ */
+ public void add(final int notificationID, String aImageUrl, String aHost, String aAlertTitle,
+ String aAlertText, PendingIntent contentIntent) {
+ // Remove the old notification with the same ID, if any
+ remove(notificationID);
+
+ final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
+ .setContentTitle(aAlertTitle)
+ .setContentText(aAlertText)
+ .setSmallIcon(R.drawable.ic_status_logo)
+ .setContentIntent(contentIntent)
+ .setAutoCancel(true)
+ .setStyle(new NotificationCompat.InboxStyle()
+ .addLine(aAlertText)
+ .setSummaryText(aHost));
+
+ // Fetch icon.
+ if (!aImageUrl.isEmpty()) {
+ final Bitmap image = BitmapUtils.decodeUrl(aImageUrl);
+ builder.setLargeIcon(image);
+ }
+
+ builder.setWhen(System.currentTimeMillis());
+ final Notification notification = builder.build();
+
+ mNotificationManager.notify(notificationID, notification);
+ mNotifications.put(notificationID, notification);
+ }
+
+ /**
+ * Adds a notification.
+ *
+ * @param id the unique ID of the notification
+ * @param notification the Notification to add
+ */
+ public void add(int id, Notification notification) {
+ mNotificationManager.notify(id, notification);
+ mNotifications.put(id, notification);
+
+ if (mForegroundNotification == null && isOngoing(notification)) {
+ setForegroundNotification(id, notification);
+ }
+ }
+
+ /**
+ * Updates a notification.
+ *
+ * @param notificationID ID of existing notification
+ * @param aProgress progress of item being updated
+ * @param aProgressMax max progress of item being updated
+ * @param aAlertText text of the notification
+ */
+ public void update(int notificationID, long aProgress, long aProgressMax, String aAlertText) {
+ Notification notification = mNotifications.get(notificationID);
+ if (notification == null) {
+ return;
+ }
+
+ notification = new NotificationCompat.Builder(mContext)
+ .setContentText(aAlertText)
+ .setSmallIcon(notification.icon)
+ .setWhen(notification.when)
+ .setContentIntent(notification.contentIntent)
+ .setProgress((int) aProgressMax, (int) aProgress, false)
+ .build();
+
+ add(notificationID, notification);
+ }
+
+ /**
+ * Removes a notification.
+ *
+ * @param notificationID ID of existing notification
+ */
+ public void remove(int notificationID) {
+ final Notification notification = mNotifications.remove(notificationID);
+ if (notification != null) {
+ updateForegroundNotification(notificationID, notification);
+ }
+ mNotificationManager.cancel(notificationID);
+ }
+
+ /**
+ * Determines whether the service is done.
+ *
+ * The service is considered finished when all notifications have been
+ * removed.
+ *
+ * @return whether all notifications have been removed
+ */
+ public boolean isDone() {
+ return mNotifications.isEmpty();
+ }
+
+ /**
+ * Determines whether a notification should hold a foreground service to keep Gecko alive
+ *
+ * @param notificationID the id of the notification to check
+ * @return whether the notification is ongoing
+ */
+ public boolean isOngoing(int notificationID) {
+ final Notification notification = mNotifications.get(notificationID);
+ return isOngoing(notification);
+ }
+
+ /**
+ * Determines whether a notification should hold a foreground service to keep Gecko alive
+ *
+ * @param notification the notification to check
+ * @return whether the notification is ongoing
+ */
+ public boolean isOngoing(Notification notification) {
+ if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) > 0) {
+ return true;
+ }
+ return false;
+ }
+
+ protected void setForegroundNotification(int id, Notification notification) {
+ mForegroundNotificationId = id;
+ mForegroundNotification = notification;
+ }
+
+ private void updateForegroundNotification(int oldId, Notification oldNotification) {
+ if (mForegroundNotificationId == oldId) {
+ // If we're removing the notification associated with the
+ // foreground, we need to pick another active notification to act
+ // as the foreground notification.
+ Notification foregroundNotification = null;
+ int foregroundId = 0;
+ for (final Integer id : mNotifications.keySet()) {
+ final Notification notification = mNotifications.get(id);
+ if (isOngoing(notification)) {
+ foregroundNotification = notification;
+ foregroundId = id;
+ break;
+ }
+ }
+ setForegroundNotification(foregroundId, foregroundNotification);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationService.java
@@ -0,0 +1,51 @@
+/* -*- 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.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+public class NotificationService extends Service {
+ private final IBinder mBinder = new NotificationBinder();
+ private NotificationHandler mHandler;
+
+ @Override
+ public void onCreate() {
+ // This has to be initialized in onCreate in order to ensure that the NotificationHandler can
+ // access the NotificationManager service.
+ mHandler = new NotificationHandler(this) {
+ @Override
+ protected void setForegroundNotification(int id, Notification notification) {
+ super.setForegroundNotification(id, notification);
+
+ if (notification == null) {
+ stopForeground(true);
+ } else {
+ startForeground(id, notification);
+ }
+ }
+ };
+ }
+
+ public class NotificationBinder extends Binder {
+ NotificationService getService() {
+ // Return this instance of NotificationService so clients can call public methods
+ return NotificationService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public NotificationHandler getNotificationHandler() {
+ return mHandler;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java
@@ -0,0 +1,308 @@
+/* -*- 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 android.support.v4.util.SimpleArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Helper class to get/set gecko prefs.
+ */
+public final class PrefsHelper {
+ private static final String LOGTAG = "GeckoPrefsHelper";
+
+ // Map pref name to ArrayList for multiple observers or PrefHandler for single observer.
+ private static final SimpleArrayMap<String, Object> OBSERVERS = new SimpleArrayMap<>();
+ private static final HashSet<String> INT_TO_STRING_PREFS = new HashSet<>(8);
+ private static final HashSet<String> INT_TO_BOOL_PREFS = new HashSet<>(2);
+
+ static {
+ INT_TO_STRING_PREFS.add("browser.chrome.titlebarMode");
+ INT_TO_STRING_PREFS.add("network.cookie.cookieBehavior");
+ INT_TO_STRING_PREFS.add("font.size.inflation.minTwips");
+ INT_TO_STRING_PREFS.add("home.sync.updateMode");
+ INT_TO_STRING_PREFS.add("browser.image_blocking");
+ INT_TO_BOOL_PREFS.add("browser.display.use_document_fonts");
+ }
+
+ @WrapForJNI
+ private static final int PREF_INVALID = -1;
+ @WrapForJNI
+ private static final int PREF_FINISH = 0;
+ @WrapForJNI
+ private static final int PREF_BOOL = 1;
+ @WrapForJNI
+ private static final int PREF_INT = 2;
+ @WrapForJNI
+ private static final int PREF_STRING = 3;
+
+ @WrapForJNI(stubName = "GetPrefs")
+ private static native void nativeGetPrefs(String[] prefNames, PrefHandler handler);
+ @WrapForJNI(stubName = "SetPref")
+ private static native void nativeSetPref(String prefName, boolean flush, int type,
+ boolean boolVal, int intVal, String strVal);
+ @WrapForJNI(stubName = "AddObserver")
+ private static native void nativeAddObserver(String[] prefNames, PrefHandler handler,
+ String[] prefsToObserve);
+ @WrapForJNI(stubName = "RemoveObserver")
+ private static native void nativeRemoveObserver(String[] prefToUnobserve);
+
+ @RobocopTarget
+ public static void getPrefs(final String[] prefNames, final PrefHandler callback) {
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeGetPrefs(prefNames, callback);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeGetPrefs",
+ String[].class, prefNames, PrefHandler.class, callback);
+ }
+ }
+
+ public static void getPref(final String prefName, final PrefHandler callback) {
+ getPrefs(new String[] { prefName }, callback);
+ }
+
+ public static void getPrefs(final ArrayList<String> prefNames, final PrefHandler callback) {
+ getPrefs(prefNames.toArray(new String[prefNames.size()]), callback);
+ }
+
+ @RobocopTarget
+ public static void setPref(final String pref, final Object value, final boolean flush) {
+ final int type;
+ boolean boolVal = false;
+ int intVal = 0;
+ String strVal = null;
+
+ if (INT_TO_STRING_PREFS.contains(pref)) {
+ // When sending to Java, we normalized special preferences that use integers
+ // and strings to represent booleans. Here, we convert them back to their
+ // actual types so we can store them.
+ type = PREF_INT;
+ intVal = Integer.parseInt(String.valueOf(value));
+ } else if (INT_TO_BOOL_PREFS.contains(pref)) {
+ type = PREF_INT;
+ intVal = (Boolean) value ? 1 : 0;
+ } else if (value instanceof Boolean) {
+ type = PREF_BOOL;
+ boolVal = (Boolean) value;
+ } else if (value instanceof Integer) {
+ type = PREF_INT;
+ intVal = (Integer) value;
+ } else {
+ type = PREF_STRING;
+ strVal = String.valueOf(value);
+ }
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeSetPref(pref, flush, type, boolVal, intVal, strVal);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeSetPref",
+ String.class, pref, flush, type, boolVal, intVal, String.class, strVal);
+ }
+ }
+
+ public static void setPref(final String pref, final Object value) {
+ setPref(pref, value, /* flush */ false);
+ }
+
+ @RobocopTarget
+ public synchronized static void addObserver(final String[] prefNames,
+ final PrefHandler handler) {
+ List<String> prefsToObserve = null;
+
+ for (String pref : prefNames) {
+ final Object existing = OBSERVERS.get(pref);
+
+ if (existing == null) {
+ // Not observing yet, so add observer.
+ if (prefsToObserve == null) {
+ prefsToObserve = new ArrayList<>(prefNames.length);
+ }
+ prefsToObserve.add(pref);
+ OBSERVERS.put(pref, handler);
+
+ } else if (existing instanceof PrefHandler) {
+ // Already observing one, so turn it into an array.
+ final List<PrefHandler> handlerList = new ArrayList<>(2);
+ handlerList.add((PrefHandler) existing);
+ handlerList.add(handler);
+ OBSERVERS.put(pref, handlerList);
+
+ } else {
+ // Already observing multiple, so add to existing array.
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ handlerList.add(handler);
+ }
+ }
+
+ final String[] namesToObserve = prefsToObserve == null ? null :
+ prefsToObserve.toArray(new String[prefsToObserve.size()]);
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeAddObserver(prefNames, handler, namesToObserve);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeAddObserver",
+ String[].class, prefNames, PrefHandler.class, handler,
+ String[].class, namesToObserve);
+ }
+ }
+
+ @RobocopTarget
+ public synchronized static void removeObserver(final PrefHandler handler) {
+ List<String> prefsToUnobserve = null;
+
+ for (int i = OBSERVERS.size() - 1; i >= 0; i--) {
+ final Object existing = OBSERVERS.valueAt(i);
+ boolean removeObserver = false;
+
+ if (existing == handler) {
+ removeObserver = true;
+
+ } else if (!(existing instanceof PrefHandler)) {
+ // Removing existing handler from list.
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ if (handlerList.remove(handler) && handlerList.isEmpty()) {
+ removeObserver = true;
+ }
+ }
+
+ if (removeObserver) {
+ // Removed last handler, so remove observer.
+ if (prefsToUnobserve == null) {
+ prefsToUnobserve = new ArrayList<>();
+ }
+ prefsToUnobserve.add(OBSERVERS.keyAt(i));
+ OBSERVERS.removeAt(i);
+ }
+ }
+
+ if (prefsToUnobserve == null) {
+ return;
+ }
+
+ final String[] namesToUnobserve =
+ prefsToUnobserve.toArray(new String[prefsToUnobserve.size()]);
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeRemoveObserver(namesToUnobserve);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeRemoveObserver",
+ String[].class, namesToUnobserve);
+ }
+ }
+
+ @WrapForJNI
+ private static void callPrefHandler(final PrefHandler handler, int type, final String pref,
+ boolean boolVal, int intVal, String strVal) {
+
+ // Some Gecko preferences use integers or strings to reference state instead of
+ // directly representing the value. Since the Java UI uses the type to determine
+ // which ui elements to show and how to handle them, we need to normalize these
+ // preferences to the correct type.
+ if (INT_TO_STRING_PREFS.contains(pref)) {
+ type = PREF_STRING;
+ strVal = String.valueOf(intVal);
+ } else if (INT_TO_BOOL_PREFS.contains(pref)) {
+ type = PREF_BOOL;
+ boolVal = intVal == 1;
+ }
+
+ switch (type) {
+ case PREF_FINISH:
+ handler.finish();
+ return;
+ case PREF_BOOL:
+ handler.prefValue(pref, boolVal);
+ return;
+ case PREF_INT:
+ handler.prefValue(pref, intVal);
+ return;
+ case PREF_STRING:
+ handler.prefValue(pref, strVal);
+ return;
+ }
+ throw new IllegalArgumentException();
+ }
+
+ @WrapForJNI
+ private synchronized static void onPrefChange(final String pref, final int type,
+ final boolean boolVal, final int intVal,
+ final String strVal) {
+ final Object existing = OBSERVERS.get(pref);
+
+ if (existing == null) {
+ return;
+ }
+
+ final Iterator<PrefHandler> itor;
+ PrefHandler handler;
+
+ if (existing instanceof PrefHandler) {
+ itor = null;
+ handler = (PrefHandler) existing;
+ } else {
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ if (handlerList.isEmpty()) {
+ return;
+ }
+ itor = handlerList.iterator();
+ handler = itor.next();
+ }
+
+ do {
+ callPrefHandler(handler, type, pref, boolVal, intVal, strVal);
+ handler.finish();
+
+ handler = itor != null && itor.hasNext() ? itor.next() : null;
+ } while (handler != null);
+ }
+
+ public interface PrefHandler {
+ void prefValue(String pref, boolean value);
+ void prefValue(String pref, int value);
+ void prefValue(String pref, String value);
+ void finish();
+ }
+
+ public static abstract class PrefHandlerBase implements PrefHandler {
+ @Override
+ public void prefValue(String pref, boolean value) {
+ throw new UnsupportedOperationException(
+ "Unhandled boolean pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void prefValue(String pref, int value) {
+ throw new UnsupportedOperationException(
+ "Unhandled int pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void prefValue(String pref, String value) {
+ throw new UnsupportedOperationException(
+ "Unhandled String pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void finish() {
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ServiceNotificationClient.java
@@ -0,0 +1,71 @@
+/* -*- 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.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * Client for posting notifications through the NotificationService.
+ */
+public class ServiceNotificationClient extends NotificationClient {
+ private static final String LOGTAG = "GeckoServiceNotificationClient";
+
+ private final ServiceConnection mConnection = new NotificationServiceConnection();
+ private boolean mBound;
+ private final Context mContext;
+
+ public ServiceNotificationClient(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected void bind() {
+ super.bind();
+ final Intent intent = new Intent(mContext, NotificationService.class);
+ mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void unbind() {
+ // If there are no more tasks and no notifications being
+ // displayed, the service is disconnected. Unfortunately,
+ // since completed download notifications are shown by
+ // removing the progress notification and creating a new
+ // static one, this will cause the service to be unbound
+ // and immediately rebound when a download completes.
+ super.unbind();
+
+ if (mBound) {
+ mBound = false;
+ mContext.unbindService(mConnection);
+ }
+ }
+
+ class NotificationServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ final NotificationService.NotificationBinder binder =
+ (NotificationService.NotificationBinder) service;
+ connectHandler(binder.getService().getNotificationHandler());
+ mBound = true;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ // This is called when the connection with the service has been
+ // unexpectedly disconnected -- that is, its process crashed.
+ // Because it is running in our same process, we should never
+ // see this happen, and the correctness of this class relies on
+ // this never happening.
+ Log.e(LOGTAG, "Notification service disconnected", new Exception());
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SmsManager.java
@@ -0,0 +1,41 @@
+/* -*- 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;
+
+public class SmsManager {
+ private static final ISmsManager sInstance;
+ static {
+ if (AppConstants.MOZ_WEBSMS_BACKEND) {
+ sInstance = new GeckoSmsManager();
+ } else {
+ sInstance = null;
+ }
+ }
+
+ public static ISmsManager getInstance() {
+ return sInstance;
+ }
+
+ public static boolean isEnabled() {
+ return AppConstants.MOZ_WEBSMS_BACKEND;
+ }
+
+ public interface ISmsManager {
+ void start();
+ void stop();
+ void shutdown();
+
+ void send(String aNumber, String aMessage, int aRequestId, boolean aShouldNotify);
+ void getMessage(int aMessageId, int aRequestId);
+ void deleteMessage(int aMessageId, int aRequestId);
+ void markMessageRead(int aMessageId, boolean aValue, boolean aSendReadReport, int aRequestId);
+ void createMessageCursor(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, String aDelivery, boolean aHasRead, boolean aRead, boolean aHasThreadId, long aThreadId, boolean aReverse, int aRequestId);
+ void createThreadCursor(int aRequestId);
+ void getNextThread(int aRequestId);
+ void getNextMessage(int aRequestId);
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceBits.java
@@ -0,0 +1,17 @@
+/* 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 java.nio.ByteBuffer;
+
+@WrapForJNI
+public class SurfaceBits {
+ public int width;
+ public int height;
+ public int format;
+ public ByteBuffer buffer;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java
@@ -0,0 +1,14 @@
+/* -*- 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 TouchEventInterceptor extends View.OnTouchListener {
+ /** Override this method for a chance to consume events before the view or its children */
+ public boolean onInterceptTouchEvent(View view, MotionEvent event);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
@@ -0,0 +1,466 @@
+/* -*- 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.gfx;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.GeckoJarReader;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UIAsyncTask;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+public final class BitmapUtils {
+ /* Default colors. */
+ private static final float[] DEFAULT_LAUNCHER_ICON_HSV = { 32.0f, 1.0f, 1.0f };
+
+ private static final String LOGTAG = "GeckoBitmapUtils";
+
+ private BitmapUtils() {}
+
+ public interface BitmapLoader {
+ public void onBitmapFound(Drawable d);
+ }
+
+ private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) {
+ if (ThreadUtils.isOnUiThread()) {
+ loader.onBitmapFound(d);
+ return;
+ }
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ loader.onBitmapFound(d);
+ }
+ });
+ }
+
+ /**
+ * Attempts to find a drawable associated with a given string, using its URI scheme to determine
+ * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and
+ * will be called with `null` if no drawable is found.
+ *
+ * The BitmapLoader `onBitmapFound` method always runs on the UI thread.
+ */
+ public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
+ if (TextUtils.isEmpty(data)) {
+ runOnBitmapFoundOnUiThread(loader, null);
+ return;
+ }
+
+ if (data.startsWith("data")) {
+ final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
+ runOnBitmapFoundOnUiThread(loader, d);
+ return;
+ }
+
+ if (data.startsWith("jar:") || data.startsWith("file://")) {
+ (new UIAsyncTask.WithoutParams<Drawable>(ThreadUtils.getBackgroundHandler()) {
+ @Override
+ public Drawable doInBackground() {
+ try {
+ if (data.startsWith("jar:jar")) {
+ return GeckoJarReader.getBitmapDrawable(
+ context, context.getResources(), data);
+ }
+
+ // Don't attempt to validate the JAR signature when loading an add-on icon
+ if (data.startsWith("jar:file")) {
+ return GeckoJarReader.getBitmapDrawable(
+ context, context.getResources(), Uri.decode(data));
+ }
+
+ final URL url = new URL(data);
+ final InputStream is = (InputStream) url.getContent();
+ try {
+ return Drawable.createFromStream(is, "src");
+ } finally {
+ is.close();
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Unable to set icon", e);
+ }
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Drawable drawable) {
+ loader.onBitmapFound(drawable);
+ }
+ }).execute();
+ return;
+ }
+
+ if (data.startsWith("-moz-icon://")) {
+ final Uri imageUri = Uri.parse(data);
+ final String ssp = imageUri.getSchemeSpecificPart();
+ final String resource = ssp.substring(ssp.lastIndexOf('/') + 1);
+
+ try {
+ final Drawable d = context.getPackageManager().getApplicationIcon(resource);
+ runOnBitmapFoundOnUiThread(loader, d);
+ } catch (Exception ex) { }
+
+ return;
+ }
+
+ if (data.startsWith("drawable://")) {
+ final Uri imageUri = Uri.parse(data);
+ final int id = getResource(imageUri, R.drawable.ic_status_logo);
+ final Drawable d = context.getResources().getDrawable(id);
+
+ runOnBitmapFoundOnUiThread(loader, d);
+ return;
+ }
+
+ runOnBitmapFoundOnUiThread(loader, null);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes) {
+ return decodeByteArray(bytes, null);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
+ return decodeByteArray(bytes, 0, bytes.length, options);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
+ return decodeByteArray(bytes, offset, length, null);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
+ if (bytes.length <= 0) {
+ throw new IllegalArgumentException("bytes.length " + bytes.length
+ + " must be a positive number");
+ }
+
+ Bitmap bitmap = null;
+ try {
+ bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
+ + ", options= " + options + ") OOM!", e);
+ return null;
+ }
+
+ if (bitmap == null) {
+ Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
+ return null;
+ }
+
+ if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
+ Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned "
+ + "a bitmap with dimensions " + bitmap.getWidth()
+ + "x" + bitmap.getHeight());
+ return null;
+ }
+
+ return bitmap;
+ }
+
+ public static Bitmap decodeStream(InputStream inputStream) {
+ try {
+ return BitmapFactory.decodeStream(inputStream);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeStream() OOM!", e);
+ return null;
+ }
+ }
+
+ public static Bitmap decodeUrl(Uri uri) {
+ return decodeUrl(uri.toString());
+ }
+
+ public static Bitmap decodeUrl(String urlString) {
+ URL url;
+
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException e) {
+ Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
+ return null;
+ }
+
+ return decodeUrl(url);
+ }
+
+ public static Bitmap decodeUrl(URL url) {
+ InputStream stream = null;
+
+ try {
+ stream = url.openStream();
+ } catch (IOException e) {
+ Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
+ return null;
+ }
+
+ if (stream == null) {
+ Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
+ return null;
+ }
+
+ Bitmap bitmap = decodeStream(stream);
+
+ try {
+ stream.close();
+ } catch (IOException e) {
+ Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
+ }
+
+ return bitmap;
+ }
+
+ public static Bitmap decodeResource(Context context, int id) {
+ return decodeResource(context, id, null);
+ }
+
+ public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) {
+ Resources resources = context.getResources();
+ try {
+ return BitmapFactory.decodeResource(resources, id, options);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e);
+ return null;
+ }
+ }
+
+ public static int getDominantColor(Bitmap source) {
+ return getDominantColor(source, true);
+ }
+
+ public static int getDominantColor(Bitmap source, boolean applyThreshold) {
+ if (source == null)
+ return Color.argb(255, 255, 255, 255);
+
+ // Keep track of how many times a hue in a given bin appears in the image.
+ // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
+ int[] colorBins = new int[36];
+
+ // The bin with the most colors. Initialize to -1 to prevent accidentally
+ // thinking the first bin holds the dominant color.
+ int maxBin = -1;
+
+ // Keep track of sum hue/saturation/value per hue bin, which we'll use to
+ // compute an average to for the dominant color.
+ float[] sumHue = new float[36];
+ float[] sumSat = new float[36];
+ float[] sumVal = new float[36];
+ float[] hsv = new float[3];
+
+ int height = source.getHeight();
+ int width = source.getWidth();
+ int[] pixels = new int[width * height];
+ source.getPixels(pixels, 0, width, 0, 0, width, height);
+ for (int row = 0; row < height; row++) {
+ for (int col = 0; col < width; col++) {
+ int c = pixels[col + row * width];
+ // Ignore pixels with a certain transparency.
+ if (Color.alpha(c) < 128)
+ continue;
+
+ Color.colorToHSV(c, hsv);
+
+ // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
+ if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
+ continue;
+
+ // We compute the dominant color by putting colors in bins based on their hue.
+ int bin = (int) Math.floor(hsv[0] / 10.0f);
+
+ // Update the sum hue/saturation/value for this bin.
+ sumHue[bin] = sumHue[bin] + hsv[0];
+ sumSat[bin] = sumSat[bin] + hsv[1];
+ sumVal[bin] = sumVal[bin] + hsv[2];
+
+ // Increment the number of colors in this bin.
+ colorBins[bin]++;
+
+ // Keep track of the bin that holds the most colors.
+ if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
+ maxBin = bin;
+ }
+ }
+
+ // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
+ if (maxBin < 0)
+ return Color.argb(255, 255, 255, 255);
+
+ // Return a color with the average hue/saturation/value of the bin with the most colors.
+ hsv[0] = sumHue[maxBin] / colorBins[maxBin];
+ hsv[1] = sumSat[maxBin] / colorBins[maxBin];
+ hsv[2] = sumVal[maxBin] / colorBins[maxBin];
+ return Color.HSVToColor(hsv);
+ }
+
+ /**
+ * Decodes a bitmap from a Base64 data URI.
+ *
+ * @param dataURI a Base64-encoded data URI string
+ * @return the decoded bitmap, or null if the data URI is invalid
+ */
+ public static Bitmap getBitmapFromDataURI(String dataURI) {
+ if (dataURI == null) {
+ return null;
+ }
+
+ byte[] raw = getBytesFromDataURI(dataURI);
+ if (raw == null || raw.length == 0) {
+ return null;
+ }
+
+ return decodeByteArray(raw);
+ }
+
+ /**
+ * Return a byte[] containing the bytes in a given base64 string, or null if this is not a valid
+ * base64 string.
+ */
+ public static byte[] getBytesFromBase64(String base64) {
+ try {
+ return Base64.decode(base64, Base64.DEFAULT);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "exception decoding bitmap from data URI: " + base64, e);
+ }
+
+ return null;
+ }
+
+ public static byte[] getBytesFromDataURI(String dataURI) {
+ final String base64 = dataURI.substring(dataURI.indexOf(',') + 1);
+ return getBytesFromBase64(base64);
+ }
+
+ public static Bitmap getBitmapFromDrawable(Drawable drawable) {
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ }
+
+ int width = drawable.getIntrinsicWidth();
+ width = width > 0 ? width : 1;
+ int height = drawable.getIntrinsicHeight();
+ height = height > 0 ? height : 1;
+
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return bitmap;
+ }
+
+ public static int getResource(Uri resourceUrl, int defaultIcon) {
+ int icon = defaultIcon;
+
+ final String scheme = resourceUrl.getScheme();
+ if ("drawable".equals(scheme)) {
+ String resource = resourceUrl.getSchemeSpecificPart();
+ resource = resource.substring(resource.lastIndexOf('/') + 1);
+
+ try {
+ return Integer.parseInt(resource);
+ } catch (NumberFormatException ex) {
+ // This isn't a resource id, try looking for a string
+ }
+
+ try {
+ final Class<R.drawable> drawableClass = R.drawable.class;
+ final Field f = drawableClass.getField(resource);
+ icon = f.getInt(null);
+ } catch (final NoSuchFieldException e1) {
+
+ // just means the resource doesn't exist for fennec. Check in Android resources
+ try {
+ final Class<android.R.drawable> drawableClass = android.R.drawable.class;
+ final Field f = drawableClass.getField(resource);
+ icon = f.getInt(null);
+ } catch (final NoSuchFieldException e2) {
+ // This drawable doesn't seem to exist...
+ } catch (Exception e3) {
+ Log.i(LOGTAG, "Exception getting drawable", e3);
+ }
+
+ } catch (Exception e4) {
+ Log.i(LOGTAG, "Exception getting drawable", e4);
+ }
+
+ resourceUrl = null;
+ }
+ return icon;
+ }
+
+ public static Bitmap getLauncherIcon(Context context, Bitmap aSource, int size) {
+ final int kOffset = 6;
+ final int kRadius = 5;
+ int insetSize = aSource != null ? size * 2 / 3 : size;
+
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+
+ // draw a base color
+ Paint paint = new Paint();
+ if (aSource == null) {
+ // If we aren't drawing a favicon, just use an orange color.
+ paint.setColor(Color.HSVToColor(DEFAULT_LAUNCHER_ICON_HSV));
+ canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
+ } else if (aSource.getWidth() >= insetSize || aSource.getHeight() >= insetSize) {
+ // Otherwise, if the icon is large enough, just draw it.
+ Rect iconBounds = new Rect(0, 0, size, size);
+ canvas.drawBitmap(aSource, null, iconBounds, null);
+ return bitmap;
+ } else {
+ // otherwise use the dominant color from the icon + a layer of transparent white to lighten it somewhat
+ int color = BitmapUtils.getDominantColor(aSource);
+ paint.setColor(color);
+ canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
+ paint.setColor(Color.argb(100, 255, 255, 255));
+ canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
+ }
+
+ // draw the overlay
+ Bitmap overlay = BitmapUtils.decodeResource(context, R.drawable.home_bg);
+ canvas.drawBitmap(overlay, null, new Rect(0, 0, size, size), null);
+
+ // draw the favicon
+ if (aSource == null)
+ aSource = BitmapUtils.decodeResource(context, R.drawable.home_star);
+
+ // by default, we scale the icon to this size
+ int sWidth = insetSize / 2;
+ int sHeight = sWidth;
+
+ int halfSize = size / 2;
+ canvas.drawBitmap(aSource,
+ null,
+ new Rect(halfSize - sWidth,
+ halfSize - sHeight,
+ halfSize + sWidth,
+ halfSize + sHeight),
+ null);
+
+ return bitmap;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
@@ -0,0 +1,771 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONArray;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+final class DisplayPortCalculator {
+ private static final String LOGTAG = "GeckoDisplayPort";
+ private static final PointF ZERO_VELOCITY = new PointF(0, 0);
+
+ private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy";
+ private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier";
+ private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x";
+ private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y";
+ private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier";
+ private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold";
+ private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr";
+ private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold";
+
+ private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null);
+
+ static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
+ }
+
+ static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ if (displayPort == null) {
+ return true;
+ }
+ return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort);
+ }
+
+ static boolean drawTimeUpdate(long millis, int pixels) {
+ return sStrategy.drawTimeUpdate(millis, pixels);
+ }
+
+ static void resetPageState() {
+ sStrategy.resetPageState();
+ }
+
+ static void initPrefs() {
+ final String[] prefs = { PREF_DISPLAYPORT_STRATEGY,
+ PREF_DISPLAYPORT_FM_MULTIPLIER,
+ PREF_DISPLAYPORT_FM_DANGER_X,
+ PREF_DISPLAYPORT_FM_DANGER_Y,
+ PREF_DISPLAYPORT_VB_MULTIPLIER,
+ PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD,
+ PREF_DISPLAYPORT_VB_REVERSE_BUFFER,
+ PREF_DISPLAYPORT_VB_DANGER_X_BASE,
+ PREF_DISPLAYPORT_VB_DANGER_Y_BASE,
+ PREF_DISPLAYPORT_VB_DANGER_X_INCR,
+ PREF_DISPLAYPORT_VB_DANGER_Y_INCR,
+ PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD };
+
+ PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
+ private final Map<String, Integer> mValues = new HashMap<String, Integer>();
+
+ @Override public void prefValue(String pref, int value) {
+ mValues.put(pref, value);
+ }
+
+ @Override public void finish() {
+ setStrategy(mValues);
+ }
+ });
+ }
+
+ /**
+ * Set the active strategy to use.
+ * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the
+ * mapping between ints and strategies.
+ */
+ static boolean setStrategy(Map<String, Integer> prefs) {
+ Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY);
+ if (strategy == null) {
+ return false;
+ }
+
+ switch (strategy) {
+ case 0:
+ sStrategy = new FixedMarginStrategy(prefs);
+ break;
+ case 1:
+ sStrategy = new VelocityBiasStrategy(prefs);
+ break;
+ case 2:
+ sStrategy = new DynamicResolutionStrategy(prefs);
+ break;
+ case 3:
+ sStrategy = new NoMarginStrategy(prefs);
+ break;
+ case 4:
+ sStrategy = new PredictionBiasStrategy(prefs);
+ break;
+ default:
+ Log.e(LOGTAG, "Invalid strategy index specified");
+ return false;
+ }
+ Log.i(LOGTAG, "Set strategy " + sStrategy.toString());
+ return true;
+ }
+
+ private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
+ Integer value = (prefs == null ? null : prefs.get(prefName));
+ return (value == null || value < 0 ? defaultValue : value) / 1000f;
+ }
+
+ private static abstract class DisplayPortStrategy {
+ /** Calculates a displayport given a viewport and panning velocity. */
+ public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity);
+ /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */
+ public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort);
+ /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */
+ public boolean drawTimeUpdate(long millis, int pixels) { return false; }
+ /** Reset any page-specific state stored, as the page being displayed has changed. */
+ public void resetPageState() {}
+ }
+
+ /**
+ * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the
+ * given metrics object. The area in the returned FloatSize may be less than width*height if the page is
+ * small, but it will never be larger than width*height.
+ * Note that this process may change the relative aspect ratio of the given dimensions.
+ */
+ private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) {
+ // figure out how much of the desired buffer amount we can actually use on the horizontal axis
+ float usableWidth = Math.min(width, metrics.getPageWidth());
+ // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and
+ // use it on the vertical axis
+ float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth);
+ float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight());
+ if (usableHeight < height && usableWidth == width) {
+ // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal
+ float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight);
+ usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth());
+ }
+ return new FloatSize(usableWidth, usableHeight);
+ }
+
+ /**
+ * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis
+ * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then
+ * clamped to page bounds and returned.
+ */
+ private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) {
+ // calculate the danger zone amounts in pixels
+ float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
+ float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
+ rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
+ // clamp to page bounds
+ return clampToPageBounds(rect, metrics);
+ }
+
+ /**
+ * Calculate the display port by expanding the viewport by the specified
+ * margins, then clamping to the page size.
+ */
+ private static DisplayPortMetrics getPageClampedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
+ float left = metrics.viewportRectLeft - margins.left;
+ float top = metrics.viewportRectTop - margins.top;
+ float right = metrics.viewportRectRight() + margins.right;
+ float bottom = metrics.viewportRectBottom() + margins.bottom;
+ left = Math.max(metrics.pageRectLeft, left);
+ top = Math.max(metrics.pageRectTop, top);
+ right = Math.min(metrics.pageRectRight, right);
+ bottom = Math.min(metrics.pageRectBottom, bottom);
+
+ return new DisplayPortMetrics(left, top, right, bottom, zoom);
+ }
+
+ /**
+ * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
+ * does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
+ * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
+ * metrics.getPageWidth(); and the same for the y axis.
+ */
+ private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) {
+ // check how much we're overflowing in each direction. note that at most one of leftOverflow
+ // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow
+ // can be greater than zero, because of the assumption described in the method javadoc.
+ float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left);
+ float rightOverflow = (metrics.viewportRectRight() + margins.right) - metrics.pageRectRight;
+ float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top);
+ float bottomOverflow = (metrics.viewportRectBottom() + margins.bottom) - metrics.pageRectBottom;
+
+ // if the margins overflow the page bounds, shift them to other side on the same axis
+ if (leftOverflow > 0) {
+ margins.left -= leftOverflow;
+ margins.right += leftOverflow;
+ } else if (rightOverflow > 0) {
+ margins.right -= rightOverflow;
+ margins.left += rightOverflow;
+ }
+ if (topOverflow > 0) {
+ margins.top -= topOverflow;
+ margins.bottom += topOverflow;
+ } else if (bottomOverflow > 0) {
+ margins.bottom -= bottomOverflow;
+ margins.top += bottomOverflow;
+ }
+ return margins;
+ }
+
+ /**
+ * Clamp the given rect to the page bounds and return it.
+ */
+ private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) {
+ if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop;
+ if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft;
+ if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight;
+ if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom;
+ return rect;
+ }
+
+ /**
+ * This class implements the variation where we basically don't bother with a a display port.
+ */
+ private static class NoMarginStrategy extends DisplayPortStrategy {
+ NoMarginStrategy(Map<String, Integer> prefs) {
+ // no prefs in this strategy
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ return new DisplayPortMetrics(metrics.viewportRectLeft,
+ metrics.viewportRectTop,
+ metrics.viewportRectRight(),
+ metrics.viewportRectBottom(),
+ metrics.zoomFactor);
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "NoMarginStrategy";
+ }
+ }
+
+ /**
+ * This class implements the variation where we use a fixed-size margin on the display port.
+ * The margin is always 300 pixels in all directions, except when we are (a) approaching a page
+ * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain
+ * the area of the display port by (a) shifting the buffer to the other side on the same axis,
+ * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on
+ * one axis.
+ */
+ private static class FixedMarginStrategy extends DisplayPortStrategy {
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private final float SIZE_MULTIPLIER;
+
+ // If the visible rect is within the danger zone (measured as a fraction of the view size
+ // from the edge of the displayport) we start redrawing to minimize checkerboarding.
+ private final float DANGER_ZONE_X_MULTIPLIER;
+ private final float DANGER_ZONE_Y_MULTIPLIER;
+
+ FixedMarginStrategy(Map<String, Integer> prefs) {
+ SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000);
+ DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100);
+ DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200);
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169). we simultaneously need to make
+ // the display port as large as possible so that we redraw less. reshape the display
+ // port dimensions to accomplish this.
+ FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+ float horizontalBuffer = usableSize.width - metrics.getWidth();
+ float verticalBuffer = usableSize.height - metrics.getHeight();
+
+ // and now calculate the display port margins based on how much buffer we've decided to use and
+ // the page bounds, ensuring we use all of the available buffer amounts on one side or the other
+ // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is
+ // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer
+ // is split).
+ RectF margins = new RectF();
+ margins.left = horizontalBuffer / 2.0f;
+ margins.right = horizontalBuffer - margins.left;
+ margins.top = verticalBuffer / 2.0f;
+ margins.bottom = verticalBuffer - margins.top;
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // Increase the size of the viewport based on the danger zone multiplier (and clamp to page
+ // boundaries), and intersect it with the current displayport to determine whether we're
+ // close to checkerboarding.
+ RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
+ return !displayPort.contains(adjustedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER;
+ }
+ }
+
+ /**
+ * This class implements the variation with a small fixed-size margin with velocity bias.
+ * In this variation, the default margins are pretty small relative to the view size, but
+ * they are affected by the panning velocity. Specifically, if we are panning on one axis,
+ * we remove the margins on the other axis because we are likely axis-locked. Also once
+ * we are panning in one direction above a certain threshold velocity, we shift the buffer
+ * so that it is almost entirely in the direction of the pan, with a little bit in the
+ * reverse direction.
+ */
+ private static class VelocityBiasStrategy extends DisplayPortStrategy {
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private final float SIZE_MULTIPLIER;
+ // The velocity above which we apply the velocity bias
+ private final float VELOCITY_THRESHOLD;
+ // How much of the buffer to keep in the reverse direction of the velocity
+ private final float REVERSE_BUFFER;
+ // If the visible rect is within the danger zone we start redrawing to minimize
+ // checkerboarding. the danger zone amount is a linear function of the form:
+ // viewportsize * (base + velocity * incr)
+ // where base and incr are configurable values.
+ private final float DANGER_ZONE_BASE_X_MULTIPLIER;
+ private final float DANGER_ZONE_BASE_Y_MULTIPLIER;
+ private final float DANGER_ZONE_INCR_X_MULTIPLIER;
+ private final float DANGER_ZONE_INCR_Y_MULTIPLIER;
+
+ VelocityBiasStrategy(Map<String, Integer> prefs) {
+ SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000);
+ VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32);
+ REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200);
+ DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000);
+ DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000);
+ DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0);
+ DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0);
+ }
+
+ /**
+ * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values.
+ * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER
+ * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the
+ * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction
+ * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the
+ * two margins on that axis.
+ */
+ private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) {
+ RectF margins = new RectF();
+
+ if (velocity.x > VELOCITY_THRESHOLD) {
+ margins.left = xAmount * REVERSE_BUFFER;
+ } else if (velocity.x < -VELOCITY_THRESHOLD) {
+ margins.left = xAmount * (1.0f - REVERSE_BUFFER);
+ } else {
+ margins.left = xAmount / 2.0f;
+ }
+ margins.right = xAmount - margins.left;
+
+ if (velocity.y > VELOCITY_THRESHOLD) {
+ margins.top = yAmount * REVERSE_BUFFER;
+ } else if (velocity.y < -VELOCITY_THRESHOLD) {
+ margins.top = yAmount * (1.0f - REVERSE_BUFFER);
+ } else {
+ margins.top = yAmount / 2.0f;
+ }
+ margins.bottom = yAmount - margins.top;
+
+ return margins;
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // but if we're panning on one axis, set the margins for the other axis to zero since we are likely
+ // axis locked and won't be displaying that extra area.
+ if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) {
+ displayPortHeight = metrics.getHeight();
+ } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) {
+ displayPortWidth = metrics.getWidth();
+ }
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169).
+ displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth());
+ displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight());
+ float horizontalBuffer = displayPortWidth - metrics.getWidth();
+ float verticalBuffer = displayPortHeight - metrics.getHeight();
+
+ // split the buffer amounts into margins based on velocity, and shift it to
+ // take into account the page bounds
+ RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity);
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // calculate the danger zone amounts based on the prefs
+ float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER));
+ float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER));
+ // clamp it such that when added to the viewport, they don't exceed page size.
+ // this is a prerequisite to calling shiftMarginsForPageBounds as we do below.
+ dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth());
+ dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight());
+
+ // split the danger zone into margins based on velocity, and ensure it doesn't exceed
+ // page bounds.
+ RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity);
+ dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics);
+
+ // we're about to checkerboard if the current viewport area + the danger zone margins
+ // fall out of the current displayport anywhere.
+ RectF adjustedViewport = new RectF(
+ metrics.viewportRectLeft - dangerMargins.left,
+ metrics.viewportRectTop - dangerMargins.top,
+ metrics.viewportRectRight() + dangerMargins.right,
+ metrics.viewportRectBottom() + dangerMargins.bottom);
+ return !displayPort.contains(adjustedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER
+ + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER
+ + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER;
+ }
+ }
+
+ /**
+ * This class implements the variation where we draw more of the page at low resolution while panning.
+ * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw
+ * resolution to compensate. This results in the same device-pixel area drawn; the compositor then
+ * scales this up to the viewport zoom level. This results in a large area of the page drawn but it
+ * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding,
+ * where we draw less but never even show it on the screen.
+ */
+ private static class DynamicResolutionStrategy extends DisplayPortStrategy {
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private static final float SIZE_MULTIPLIER = 1.5f;
+
+ // The velocity above which we start zooming out the display port to keep up
+ // with the panning.
+ private static final float VELOCITY_EXPANSION_THRESHOLD = GeckoAppShell.getDpi() / 16f;
+
+ // How much we increase the display port based on velocity. Assuming no friction and
+ // splitting (see below), this should be be the number of frames (@60fps) between us
+ // calculating the display port and the draw of the *next* display port getting composited
+ // and displayed on the screen. This is because the timeline looks like this:
+ // Java: pan pan pan pan pan pan ! pan pan pan pan pan pan !
+ // Gecko: \-> draw -> composite / \-> draw -> composite /
+ // The display port calculated on the first "pan" gets composited to the screen at the
+ // first exclamation mark, and remains on the screen until the second exclamation mark.
+ // In order to avoid checkerboarding, that display port must be able to contain all of
+ // the panning until the second exclamation mark, which encompasses two entire draw/composite
+ // cycles.
+ // If we take into account friction, our velocity multiplier should be reduced as the
+ // amount of pan will decrease each time. If we take into account display port splitting,
+ // it should be increased as the splitting means some of the display port will be used to
+ // draw in the opposite direction of the velocity. For now I'm assuming these two cancel
+ // each other out.
+ private static final float VELOCITY_MULTIPLIER = 60.0f;
+
+ // The following constants adjust how biased the display port is in the direction of panning.
+ // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the
+ // display port "buffer" area, otherwise we use the slow split factor. This is based on the
+ // assumption that if the user is panning fast, they are less likely to reverse directions
+ // and go backwards, so we should spend more of our display port buffer in the direction of
+ // panning.
+ private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f;
+ private static final float FAST_SPLIT_FACTOR = 0.95f;
+ private static final float SLOW_SPLIT_FACTOR = 0.8f;
+
+ // The following constants are used for viewport prediction; we use them to estimate where
+ // the viewport will be soon and whether or not we should trigger a draw right now. "soon"
+ // in the previous sentence really refers to the amount of time it would take to draw and
+ // composite from the point at which we do the calculation, and that is not really a known
+ // quantity. The velocity multiplier is how much we multiply the velocity by; it has the
+ // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account
+ // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the
+ // viewport size that we use as an extra "danger zone" around the viewport; if this danger
+ // zone falls outside the display port then we are approaching the point at which we will
+ // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is
+ // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the
+ // danger zone, and thus will be constantly drawing.
+ private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f;
+ private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f)
+
+ DynamicResolutionStrategy(Map<String, Integer> prefs) {
+ // ignore prefs for now
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // for resolution calculation purposes, we need to know what the adjusted display port dimensions
+ // would be if we had zero velocity, so calculate that here before we increase the display port
+ // based on velocity.
+ FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+
+ // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their
+ // relative aspect ratio.
+ if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) {
+ float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth,
+ Math.abs(velocity.y) / displayPortHeight);
+ velocityFactor *= VELOCITY_MULTIPLIER;
+
+ displayPortWidth += (displayPortWidth * velocityFactor);
+ displayPortHeight += (displayPortHeight * velocityFactor);
+ }
+
+ // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels)
+ // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied
+ // by metrics.zoomFactor
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169). we simultaneously need to make
+ // the display port as large as possible so that we redraw less. reshape the display
+ // port dimensions to accomplish this. this may change the aspect ratio of the display port,
+ // but we are assuming that this is desirable because the advantages from pre-drawing will
+ // outweigh the disadvantages from any buffer reallocations that might occur.
+ FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+ float horizontalBuffer = usableSize.width - metrics.getWidth();
+ float verticalBuffer = usableSize.height - metrics.getHeight();
+
+ // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have.
+ // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case
+ // the user scrolls there. we now need to split the buffer area on each axis so that we know
+ // what the exact margins on each side will be. first we split the buffer amount based on the direction
+ // we're moving, so that we have a larger buffer in the direction of travel.
+ RectF margins = new RectF();
+ margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x);
+ margins.right = horizontalBuffer - margins.left;
+ margins.top = splitBufferByVelocity(verticalBuffer, velocity.y);
+ margins.bottom = verticalBuffer - margins.top;
+
+ // then, we account for running into the page bounds - so that if we hit the top of the page, we need
+ // to drop the top margin and move that amount to the bottom margin.
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ // finally, we calculate the resolution we want to render the display port area at. We do this
+ // so that as we expand the display port area (because of velocity), we reduce the resolution of
+ // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate
+ // the reduction in resolution by comparing the display port size with and without the velocity
+ // changes applied.
+ // this effectively means that as we pan faster and faster, the display port grows, but we paint
+ // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing
+ // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone.
+ // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we
+ // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor.
+ float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height);
+ float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor);
+
+ DisplayPortMetrics dpMetrics = new DisplayPortMetrics(
+ metrics.viewportRectLeft - margins.left,
+ metrics.viewportRectTop - margins.top,
+ metrics.viewportRectRight() + margins.right,
+ metrics.viewportRectBottom() + margins.bottom,
+ displayResolution);
+ return dpMetrics;
+ }
+
+ /**
+ * Split the given buffer amount into two based on the velocity.
+ * Given an amount of total usable buffer on an axis, this will
+ * return the amount that should be used on the left/top side of
+ * the axis (the side which a negative velocity vector corresponds
+ * to).
+ */
+ private float splitBufferByVelocity(float amount, float velocity) {
+ // if no velocity, so split evenly
+ if (FloatUtils.fuzzyEquals(velocity, 0)) {
+ return amount / 2.0f;
+ }
+ // if we're moving quickly, assign more of the amount in that direction
+ // since is is less likely that we will reverse direction immediately
+ if (velocity < -VELOCITY_FAST_THRESHOLD) {
+ return amount * FAST_SPLIT_FACTOR;
+ }
+ if (velocity > VELOCITY_FAST_THRESHOLD) {
+ return amount * (1.0f - FAST_SPLIT_FACTOR);
+ }
+ // if we're moving slowly, then assign less of the amount in that direction
+ if (velocity < 0) {
+ return amount * SLOW_SPLIT_FACTOR;
+ } else {
+ return amount * (1.0f - SLOW_SPLIT_FACTOR);
+ }
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // Expand the viewport based on our velocity (and clamp it to page boundaries).
+ // Then intersect it with the last-requested displayport to determine whether we're
+ // close to checkerboarding.
+
+ RectF predictedViewport = metrics.getViewport();
+
+ // first we expand the viewport in the direction we're moving based on some
+ // multiple of the current velocity.
+ if (velocity.length() > 0) {
+ if (velocity.x < 0) {
+ predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
+ } else if (velocity.x > 0) {
+ predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
+ }
+
+ if (velocity.y < 0) {
+ predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
+ } else if (velocity.y > 0) {
+ predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
+ }
+ }
+
+ // then we expand the viewport evenly in all directions just to have an extra
+ // safety zone. this also clamps it to page bounds.
+ predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics);
+ return !displayPort.contains(predictedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "DynamicResolutionStrategy";
+ }
+ }
+
+ /**
+ * This class implements the variation where we use the draw time to predict where we will be when
+ * a draw completes, and draw that instead of where we are now. In this variation, when our panning
+ * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can
+ * pan in any direction without encountering checkerboarding.
+ * Once the user is panning, we modify the displayport to encompass an area range of where we think
+ * the user will be when the draw completes. This heuristic relies on both the estimated draw time
+ * the panning velocity; unexpected changes in either of these values will cause the heuristic to
+ * fail and show checkerboard.
+ */
+ private static class PredictionBiasStrategy extends DisplayPortStrategy {
+ private static float VELOCITY_THRESHOLD;
+
+ private int mPixelArea; // area of the viewport, used in draw time calculations
+ private int mMinFramesToDraw; // minimum number of frames we take to draw
+ private int mMaxFramesToDraw; // maximum number of frames we take to draw
+
+ PredictionBiasStrategy(Map<String, Integer> prefs) {
+ VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16);
+ resetPageState();
+ }
+
+ @Override
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float width = metrics.getWidth();
+ float height = metrics.getHeight();
+ mPixelArea = (int)(width * height);
+
+ if (velocity.length() < VELOCITY_THRESHOLD) {
+ // if we're going slow, expand the displayport to 9x viewport size
+ RectF margins = new RectF(width, height, width, height);
+ return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ // figure out how far we expect to be
+ float minDx = velocity.x * mMinFramesToDraw;
+ float minDy = velocity.y * mMinFramesToDraw;
+ float maxDx = velocity.x * mMaxFramesToDraw;
+ float maxDy = velocity.y * mMaxFramesToDraw;
+
+ // figure out how many pixels we will be drawing when we draw the above-calculated range.
+ // this will be larger than the viewport area.
+ float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy));
+ // adjust how far we will get because of the time spent drawing all these extra pixels. this
+ // will again increase the number of pixels drawn so really we could keep iterating this over
+ // and over, but once seems enough for now.
+ maxDx = maxDx * pixelsToDraw / mPixelArea;
+ maxDy = maxDy * pixelsToDraw / mPixelArea;
+
+ // and finally generate the displayport. the min/max stuff takes care of
+ // negative velocities as well as positive.
+ RectF margins = new RectF(
+ -Math.min(minDx, maxDx),
+ -Math.min(minDy, maxDy),
+ Math.max(minDx, maxDx),
+ Math.max(minDy, maxDy));
+ return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ @Override
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs.
+ // refer to the comments in calculate() to understand what this is doing.
+ float minDx = velocity.x * mMinFramesToDraw;
+ float minDy = velocity.y * mMinFramesToDraw;
+ float maxDx = velocity.x * mMaxFramesToDraw;
+ float maxDy = velocity.y * mMaxFramesToDraw;
+ float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy));
+ maxDx = maxDx * pixelsToDraw / mPixelArea;
+ maxDy = maxDy * pixelsToDraw / mPixelArea;
+
+ // now that we have an idea of how far we will be when the draw completes, take the farthest
+ // end of that range and see if it falls outside the displayport bounds. if it does, allow
+ // the draw to go through
+ RectF predictedViewport = metrics.getViewport();
+ predictedViewport.left += maxDx;
+ predictedViewport.top += maxDy;
+ predictedViewport.right += maxDx;
+ predictedViewport.bottom += maxDy;
+
+ predictedViewport = clampToPageBounds(predictedViewport, metrics);
+ return !displayPort.contains(predictedViewport);
+ }
+
+ @Override
+ public boolean drawTimeUpdate(long millis, int pixels) {
+ // calculate the number of frames it took to draw a viewport-sized area
+ float normalizedTime = (float)mPixelArea * millis / pixels;
+ int normalizedFrames = (int) Math.ceil(normalizedTime * 60f / 1000f);
+ // broaden our range on how long it takes to draw if the draw falls outside
+ // the range. this allows it to grow gradually. this heuristic may need to
+ // be tweaked into more of a floating window average or something.
+ if (normalizedFrames <= mMinFramesToDraw) {
+ mMinFramesToDraw--;
+ } else if (normalizedFrames > mMaxFramesToDraw) {
+ mMaxFramesToDraw++;
+ } else {
+ return true;
+ }
+ Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]");
+ return true;
+ }
+
+ @Override
+ public void resetPageState() {
+ mMinFramesToDraw = 0;
+ mMaxFramesToDraw = 2;
+ }
+
+ @Override
+ public String toString() {
+ return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
@@ -0,0 +1,78 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.RectF;
+
+/*
+ * This class keeps track of the area we request Gecko to paint, as well
+ * as the resolution of the paint. The area may be different from the visible
+ * area of the page, and the resolution may be different from the resolution
+ * used in the compositor to render the page. This is so that we can ask Gecko
+ * to paint a much larger area without using extra memory, and then render some
+ * subsection of that with compositor scaling.
+ */
+public final class DisplayPortMetrics {
+ @WrapForJNI
+ public final float resolution;
+ @WrapForJNI
+ private final RectF mPosition;
+
+ public DisplayPortMetrics() {
+ this(0, 0, 0, 0, 1);
+ }
+
+ @WrapForJNI
+ public DisplayPortMetrics(float left, float top, float right, float bottom, float resolution) {
+ this.resolution = resolution;
+ mPosition = new RectF(left, top, right, bottom);
+ }
+
+ public float getLeft() {
+ return mPosition.left;
+ }
+
+ public float getTop() {
+ return mPosition.top;
+ }
+
+ public float getRight() {
+ return mPosition.right;
+ }
+
+ public float getBottom() {
+ return mPosition.bottom;
+ }
+
+ public boolean contains(RectF rect) {
+ return mPosition.contains(rect);
+ }
+
+ public boolean fuzzyEquals(DisplayPortMetrics metrics) {
+ return RectUtils.fuzzyEquals(mPosition, metrics.mPosition)
+ && FloatUtils.fuzzyEquals(resolution, metrics.resolution);
+ }
+
+ public String toJSON() {
+ StringBuilder sb = new StringBuilder(256);
+ sb.append("{ \"left\": ").append(mPosition.left)
+ .append(", \"top\": ").append(mPosition.top)
+ .append(", \"right\": ").append(mPosition.right)
+ .append(", \"bottom\": ").append(mPosition.bottom)
+ .append(", \"resolution\": ").append(resolution)
+ .append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ return "DisplayPortMetrics v=(" + mPosition.left + "," + mPosition.top + "," + mPosition.right + ","
+ + mPosition.bottom + ") z=" + resolution;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DrawTimingQueue.java
@@ -0,0 +1,94 @@
+/* -*- 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.gfx;
+
+import android.os.SystemClock;
+
+/**
+ * A custom-built data structure to assist with measuring draw times.
+ *
+ * This class maintains a fixed-size circular buffer of DisplayPortMetrics
+ * objects and associated timestamps. It provides only three operations, which
+ * is all we require for our purposes of measuring draw times. Note
+ * in particular that the class is designed so that even though it is
+ * accessed from multiple threads, it does not require synchronization;
+ * any concurrency errors that result from this are handled gracefully.
+ *
+ * Assuming an unrolled buffer so that mTail is greater than mHead, the data
+ * stored in the buffer at entries [mHead, mTail) will never be modified, and
+ * so are "safe" to read. If this reading is done on the same thread that
+ * owns mHead, then reading the range [mHead, mTail) is guaranteed to be safe
+ * since the range itself will not shrink.
+ */
+final class DrawTimingQueue {
+ private static final String LOGTAG = "GeckoDrawTimingQueue";
+ private static final int BUFFER_SIZE = 16;
+
+ private final DisplayPortMetrics[] mMetrics;
+ private final long[] mTimestamps;
+
+ private int mHead;
+ private int mTail;
+
+ DrawTimingQueue() {
+ mMetrics = new DisplayPortMetrics[BUFFER_SIZE];
+ mTimestamps = new long[BUFFER_SIZE];
+ mHead = BUFFER_SIZE - 1;
+ }
+
+ /**
+ * Add a new entry to the tail of the queue. If the buffer is full,
+ * do nothing. This must only be called from the Java UI thread.
+ */
+ boolean add(DisplayPortMetrics metrics) {
+ if (mHead == mTail) {
+ return false;
+ }
+ mMetrics[mTail] = metrics;
+ mTimestamps[mTail] = SystemClock.uptimeMillis();
+ mTail = (mTail + 1) % BUFFER_SIZE;
+ return true;
+ }
+
+ /**
+ * Find the timestamp associated with the given metrics, AND remove
+ * all metrics objects from the start of the queue up to and including
+ * the one provided. Note that because of draw coalescing, the metrics
+ * object passed in here may not be the one at the head of the queue,
+ * and so we must iterate our way through the list to find it.
+ * This must only be called from the compositor thread.
+ */
+ long findTimeFor(DisplayPortMetrics metrics) {
+ // keep a copy of the tail pointer so that we ignore new items
+ // added to the queue while we are searching. this is fine because
+ // the one we are looking for will either have been added already
+ // or will not be in the queue at all.
+ int tail = mTail;
+ // walk through the "safe" range from mHead to tail; these entries
+ // will not be modified unless we change mHead.
+ int i = (mHead + 1) % BUFFER_SIZE;
+ while (i != tail) {
+ if (mMetrics[i].fuzzyEquals(metrics)) {
+ // found it, copy out the timestamp to a local var BEFORE
+ // changing mHead or add could clobber the timestamp.
+ long timestamp = mTimestamps[i];
+ mHead = i;
+ return timestamp;
+ }
+ i = (i + 1) % BUFFER_SIZE;
+ }
+ return -1;
+ }
+
+ /**
+ * Reset the buffer to empty.
+ * This must only be called from the compositor thread.
+ */
+ void reset() {
+ // we can only modify mHead on this thread.
+ mHead = (mTail + BUFFER_SIZE - 1) % BUFFER_SIZE;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
@@ -0,0 +1,605 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.graphics.PointF;
+import android.support.v4.view.ViewCompat;
+import android.util.Log;
+import android.view.animation.DecelerateInterpolator;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+public class DynamicToolbarAnimator {
+ private static final String LOGTAG = "GeckoDynamicToolbarAnimator";
+ private static final String PREF_SCROLL_TOOLBAR_THRESHOLD = "browser.ui.scroll-toolbar-threshold";
+
+ public static enum PinReason {
+ RELAYOUT,
+ ACTION_MODE,
+ FULL_SCREEN,
+ CARET_DRAG
+ }
+
+ private final Set<PinReason> pinFlags = Collections.synchronizedSet(EnumSet.noneOf(PinReason.class));
+
+ // The duration of the animation in ns
+ private static final long ANIMATION_DURATION = 250000000;
+
+ private final GeckoLayerClient mTarget;
+ private final List<LayerView.DynamicToolbarListener> mListeners;
+
+ /* The translation to be applied to the toolbar UI view. This is the
+ * distance from the default/initial location (at the top of the screen,
+ * visible to the user) to where we want it to be. This variable should
+ * always be between 0 (toolbar fully visible) and the height of the toolbar
+ * (toolbar fully hidden), inclusive.
+ */
+ private float mToolbarTranslation;
+
+ /* The translation to be applied to the LayerView. This is the distance from
+ * the default/initial location (just below the toolbar, with the bottom
+ * extending past the bottom of the screen) to where we want it to be.
+ * This variable should always be between 0 and the height of the toolbar,
+ * inclusive.
+ */
+ private float mLayerViewTranslation;
+
+ /* This stores the maximum translation that can be applied to the toolbar
+ * and layerview when scrolling. This is populated with the height of the
+ * toolbar. */
+ private float mMaxTranslation;
+
+ /* This interpolator is used for the above mentioned animation */
+ private DecelerateInterpolator mInterpolator;
+
+ /* This is the proportion of the viewport rect that needs to be travelled
+ * while scrolling before the translation will start taking effect.
+ */
+ private float SCROLL_TOOLBAR_THRESHOLD = 0.20f;
+ /* The ID of the prefs listener for the scroll-toolbar threshold */
+ private final PrefsHelper.PrefHandler mPrefObserver;
+
+ /* While we are resizing the viewport to account for the toolbar, the Java
+ * code and painted layer metrics in the compositor have different notions
+ * of the CSS viewport height. The Java value is stored in the
+ * GeckoLayerClient's viewport metrics, and the Gecko one is stored here.
+ * This allows us to adjust fixed-pos items correctly.
+ * You must synchronize on mTarget.getLock() to read/write this. */
+ private Integer mHeightDuringResize;
+
+ /* This tracks if we should trigger a "snap" on the next composite. A "snap"
+ * is when we simultaneously move the LayerView and change the scroll offset
+ * in the compositor so that everything looks the same on the screen but
+ * has really been shifted.
+ * You must synchronize on |this| to read/write this. */
+ private boolean mSnapRequired = false;
+
+ /* The task that handles showing/hiding toolbar */
+ private DynamicToolbarAnimationTask mAnimationTask;
+
+ /* The start point of a drag, used for scroll-based dynamic toolbar
+ * behaviour. */
+ private PointF mTouchStart;
+ private float mLastTouch;
+
+ /* Set to true when root content is being scrolled */
+ private boolean mScrollingRootContent;
+
+ public DynamicToolbarAnimator(GeckoLayerClient aTarget) {
+ mTarget = aTarget;
+ mListeners = new ArrayList<LayerView.DynamicToolbarListener>();
+
+ mInterpolator = new DecelerateInterpolator();
+
+ // Listen to the dynamic toolbar pref
+ mPrefObserver = new PrefsHelper.PrefHandlerBase() {
+ @Override
+ public void prefValue(String pref, int value) {
+ SCROLL_TOOLBAR_THRESHOLD = value / 100.0f;
+ }
+ };
+ PrefsHelper.addObserver(new String[] { PREF_SCROLL_TOOLBAR_THRESHOLD }, mPrefObserver);
+
+ // JPZ doesn't notify when scrolling root content. This maintains existing behaviour.
+ if (!AppConstants.MOZ_ANDROID_APZ) {
+ mScrollingRootContent = true;
+ }
+ }
+
+ public void destroy() {
+ PrefsHelper.removeObserver(mPrefObserver);
+ }
+
+ public void addTranslationListener(LayerView.DynamicToolbarListener aListener) {
+ mListeners.add(aListener);
+ }
+
+ public void removeTranslationListener(LayerView.DynamicToolbarListener aListener) {
+ mListeners.remove(aListener);
+ }
+
+ private void fireListeners() {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onTranslationChanged(mToolbarTranslation, mLayerViewTranslation);
+ }
+ }
+
+ void onPanZoomStopped() {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onPanZoomStopped();
+ }
+ }
+
+ void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onMetricsChanged(aMetrics);
+ }
+ }
+
+ public void setMaxTranslation(float maxTranslation) {
+ ThreadUtils.assertOnUiThread();
+ if (maxTranslation < 0) {
+ Log.e(LOGTAG, "Got a negative max-translation value: " + maxTranslation + "; clamping to zero");
+ mMaxTranslation = 0;
+ } else {
+ mMaxTranslation = maxTranslation;
+ }
+ }
+
+ public float getMaxTranslation() {
+ return mMaxTranslation;
+ }
+
+ public float getToolbarTranslation() {
+ return mToolbarTranslation;
+ }
+
+ /**
+ * If true, scroll changes will not affect translation.
+ */
+ public boolean isPinned() {
+ return !pinFlags.isEmpty();
+ }
+
+ public boolean isPinnedBy(PinReason reason) {
+ return pinFlags.contains(reason);
+ }
+
+ public void setPinned(boolean pinned, PinReason reason) {
+ if (pinned) {
+ pinFlags.add(reason);
+ } else {
+ pinFlags.remove(reason);
+ }
+ }
+
+ public void showToolbar(boolean immediately) {
+ animateToolbar(true, immediately);
+ }
+
+ public void hideToolbar(boolean immediately) {
+ animateToolbar(false, immediately);
+ }
+
+ public void setScrollingRootContent(boolean isRootContent) {
+ mScrollingRootContent = isRootContent;
+ }
+
+ private void animateToolbar(final boolean showToolbar, boolean immediately) {
+ ThreadUtils.assertOnUiThread();
+
+ if (mAnimationTask != null) {
+ mTarget.getView().removeRenderTask(mAnimationTask);
+ mAnimationTask = null;
+ }
+
+ float desiredTranslation = (showToolbar ? 0 : mMaxTranslation);
+ Log.v(LOGTAG, "Requested " + (immediately ? "immediate " : "") + "toolbar animation to translation " + desiredTranslation);
+ if (FloatUtils.fuzzyEquals(mToolbarTranslation, desiredTranslation)) {
+ // If we're already pretty much in the desired position, don't bother
+ // with a full animation; do an immediate jump
+ immediately = true;
+ Log.v(LOGTAG, "Changing animation to immediate jump");
+ }
+
+ if (showToolbar && immediately) {
+ // Special case for showing the toolbar immediately: some of the call
+ // sites expect this to happen synchronously, so let's do that. This
+ // is safe because if we are showing the toolbar from a hidden state
+ // there is no chance of showing garbage
+ mToolbarTranslation = desiredTranslation;
+ fireListeners();
+ // And then proceed with the normal flow (some of which will be
+ // a no-op now)...
+ }
+
+ if (!showToolbar) {
+ // If we are hiding the toolbar, we need to move the LayerView first,
+ // so that we don't end up showing garbage under the toolbar when
+ // it is hidden. In the case that we are showing the toolbar, we
+ // move the LayerView after the toolbar is shown - the
+ // DynamicToolbarAnimationTask calls that upon completion.
+ shiftLayerView(desiredTranslation);
+ }
+
+ mAnimationTask = new DynamicToolbarAnimationTask(desiredTranslation, immediately, showToolbar);
+ mTarget.getView().postRenderTask(mAnimationTask);
+ }
+
+ private synchronized void shiftLayerView(float desiredTranslation) {
+ float layerViewTranslationNeeded = desiredTranslation - mLayerViewTranslation;
+ mLayerViewTranslation = desiredTranslation;
+ synchronized (mTarget.getLock()) {
+ mHeightDuringResize = new Integer(mTarget.getViewportMetrics().viewportRectHeight);
+ mSnapRequired = mTarget.setViewportSize(
+ mTarget.getView().getWidth(),
+ mTarget.getView().getHeight() - Math.round(mMaxTranslation - mLayerViewTranslation),
+ new PointF(0, -layerViewTranslationNeeded));
+ if (!mSnapRequired) {
+ mHeightDuringResize = null;
+ ThreadUtils.postToUiThread(new Runnable() {
+ // Post to run it outside of the synchronize blocks. The
+ // delay shouldn't hurt.
+ @Override
+ public void run() {
+ fireListeners();
+ }
+ });
+ }
+ // Request a composite, which will trigger the snap.
+ mTarget.getView().requestRender();
+ }
+ }
+
+ IntSize getViewportSize() {
+ ThreadUtils.assertOnUiThread();
+
+ int viewWidth = mTarget.getView().getWidth();
+ int viewHeight = mTarget.getView().getHeight();
+ float toolbarTranslation = mToolbarTranslation;
+ if (mAnimationTask != null) {
+ // If we have an animation going, mToolbarTranslation may be in flux
+ // and we should use the final value it will settle on.
+ toolbarTranslation = mAnimationTask.getFinalToolbarTranslation();
+ }
+ int viewHeightVisible = viewHeight - Math.round(mMaxTranslation - toolbarTranslation);
+ return new IntSize(viewWidth, viewHeightVisible);
+ }
+
+ boolean isResizing() {
+ return mHeightDuringResize != null;
+ }
+
+ private final Runnable mSnapRunnable = new Runnable() {
+ private int mFrame = 0;
+
+ @Override
+ public final void run() {
+ // It takes 2 frames for the view translation to take effect, at
+ // least on a Nexus 4 device running Android 4.2.2. So we wait for
+ // two frames before doing the notifyAll(), otherwise we get a
+ // short user-visible glitch.
+ // TODO: find a better way to do this, if possible.
+ if (mFrame == 1) {
+ synchronized (this) {
+ this.notifyAll();
+ }
+ mFrame = 0;
+ return;
+ }
+
+ if (mFrame == 0) {
+ fireListeners();
+ }
+
+ ViewCompat.postOnAnimation(mTarget.getView(), this);
+ mFrame++;
+ }
+ };
+
+ void scrollChangeResizeCompleted() {
+ synchronized (mTarget.getLock()) {
+ Log.v(LOGTAG, "Scrollchange resize completed");
+ mHeightDuringResize = null;
+ }
+ }
+
+ /**
+ * "Shrinks" the absolute value of aValue by moving it closer to zero by
+ * aShrinkAmount, but prevents it from crossing over zero. If aShrinkAmount
+ * is negative it is ignored.
+ * @return The shrunken value.
+ */
+ private static float shrinkAbs(float aValue, float aShrinkAmount) {
+ if (aShrinkAmount <= 0) {
+ return aValue;
+ }
+ float shrinkBy = Math.min(Math.abs(aValue), aShrinkAmount);
+ return (aValue < 0 ? aValue + shrinkBy : aValue - shrinkBy);
+ }
+
+ /**
+ * This function takes in a scroll amount and decides how much of that
+ * should be used up to translate things on screen because of the dynamic
+ * toolbar behaviour. It returns the maximum amount that could be used
+ * for translation purposes; the rest must be used for scrolling.
+ */
+ private float decideTranslation(float aDelta,
+ ImmutableViewportMetrics aMetrics,
+ float aTouchTravelDistance) {
+
+ float exposeThreshold = aMetrics.getHeight() * SCROLL_TOOLBAR_THRESHOLD;
+ float translation = aDelta;
+
+ if (translation < 0) { // finger moving upwards
+ translation = shrinkAbs(translation, aMetrics.getOverscroll().top);
+
+ // If the toolbar is in a state between fully hidden and fully shown
+ // (i.e. the user is actively translating it), then we want the
+ // translation to take effect right away. Or if the user has moved
+ // their finger past the required threshold (and is not trying to
+ // scroll past the bottom of the page) then also we want the touch
+ // to cause translation. If the toolbar is fully visible, we only
+ // want the toolbar to hide if the user is scrolling the root content.
+ boolean inBetween = (mToolbarTranslation != 0 && mToolbarTranslation != mMaxTranslation);
+ boolean reachedThreshold = -aTouchTravelDistance >= exposeThreshold;
+ boolean atBottomOfPage = aMetrics.viewportRectBottom() >= aMetrics.pageRectBottom;
+ if (inBetween || (mScrollingRootContent && reachedThreshold && !atBottomOfPage)) {
+ return translation;
+ }
+ } else { // finger moving downwards
+ translation = shrinkAbs(translation, aMetrics.getOverscroll().bottom);
+
+ // Ditto above comment, but in this case if they reached the top and
+ // the toolbar is not shown, then we do want to allow translation
+ // right away.
+ boolean inBetween = (mToolbarTranslation != 0 && mToolbarTranslation != mMaxTranslation);
+ boolean reachedThreshold = aTouchTravelDistance >= exposeThreshold;
+ boolean atTopOfPage = aMetrics.viewportRectTop <= aMetrics.pageRectTop;
+ boolean isToolbarTranslated = (mToolbarTranslation != 0);
+ if (inBetween || reachedThreshold || (atTopOfPage && isToolbarTranslated)) {
+ return translation;
+ }
+ }
+
+ return 0;
+ }
+
+ // Timestamp of the start of the touch event used to calculate toolbar velocity
+ private long mLastEventTime;
+ // Current velocity of the toolbar. Used to populate the velocity queue in C++APZ.
+ private float mVelocity;
+
+ boolean onInterceptTouchEvent(MotionEvent event) {
+ if (isPinned()) {
+ return false;
+ }
+
+ // Animations should never co-exist with the user touching the screen.
+ if (mAnimationTask != null) {
+ mTarget.getView().removeRenderTask(mAnimationTask);
+ mAnimationTask = null;
+ }
+
+ // we only care about single-finger drags here; any other kind of event
+ // should reset and cause us to start over.
+ if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE ||
+ event.getActionMasked() != MotionEvent.ACTION_MOVE ||
+ event.getPointerCount() != 1)
+ {
+ if (mTouchStart != null) {
+ Log.v(LOGTAG, "Resetting touch sequence due to non-move");
+ mTouchStart = null;
+ mVelocity = 0.0f;
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ // We need to do this even if the toolbar is already fully
+ // visible or fully hidden, because this is what triggers the
+ // viewport resize in content and updates the viewport metrics.
+ boolean toolbarMostlyVisible = mToolbarTranslation < (mMaxTranslation / 2);
+ Log.v(LOGTAG, "All fingers lifted, completing " + (toolbarMostlyVisible ? "show" : "hide"));
+ animateToolbar(toolbarMostlyVisible, false);
+ }
+ return false;
+ }
+
+ if (mTouchStart != null) {
+ float prevDir = mLastTouch - mTouchStart.y;
+ float newDir = event.getRawY() - mLastTouch;
+ if (prevDir != 0 && newDir != 0 && ((prevDir < 0) != (newDir < 0))) {
+ // If the direction of movement changed, reset the travel
+ // distance properties.
+ mTouchStart = null;
+ mVelocity = 0.0f;
+ }
+ }
+
+ if (mTouchStart == null) {
+ mTouchStart = new PointF(event.getRawX(), event.getRawY());
+ mLastTouch = event.getRawY();
+ mLastEventTime = event.getEventTime();
+ return false;
+ }
+
+ float deltaY = event.getRawY() - mLastTouch;
+ long currentTime = event.getEventTime();
+ float deltaTime = (float)(currentTime - mLastEventTime);
+ mLastEventTime = currentTime;
+ if (deltaTime > 0.0f) {
+ mVelocity = -deltaY / deltaTime;
+ } else {
+ mVelocity = 0.0f;
+ }
+ mLastTouch = event.getRawY();
+ float travelDistance = event.getRawY() - mTouchStart.y;
+
+ ImmutableViewportMetrics metrics = mTarget.getViewportMetrics();
+
+ if (metrics.getPageHeight() <= mTarget.getView().getHeight() &&
+ mToolbarTranslation == 0) {
+ // If the page is short and the toolbar is already visible, don't
+ // allow translating it out of view.
+ return false;
+ }
+
+ float translation = decideTranslation(deltaY, metrics, travelDistance);
+
+ float oldToolbarTranslation = mToolbarTranslation;
+ float oldLayerViewTranslation = mLayerViewTranslation;
+ mToolbarTranslation = FloatUtils.clamp(mToolbarTranslation - translation, 0, mMaxTranslation);
+ mLayerViewTranslation = FloatUtils.clamp(mLayerViewTranslation - translation, 0, mMaxTranslation);
+
+ if (oldToolbarTranslation == mToolbarTranslation &&
+ oldLayerViewTranslation == mLayerViewTranslation) {
+ return false;
+ }
+
+ if (mToolbarTranslation == mMaxTranslation) {
+ Log.v(LOGTAG, "Toolbar at maximum translation, calling shiftLayerView(" + mMaxTranslation + ")");
+ shiftLayerView(mMaxTranslation);
+ } else if (mToolbarTranslation == 0) {
+ Log.v(LOGTAG, "Toolbar at minimum translation, calling shiftLayerView(0)");
+ shiftLayerView(0);
+ }
+
+ fireListeners();
+ mTarget.getView().requestRender();
+ return true;
+ }
+
+ // Get the current velocity of the toolbar.
+ float getVelocity() {
+ return mVelocity;
+ }
+
+ public PointF getVisibleEndOfLayerView() {
+ return new PointF(mTarget.getView().getWidth(),
+ mTarget.getView().getHeight() - mMaxTranslation + mLayerViewTranslation);
+ }
+
+ private float bottomOfCssViewport(ImmutableViewportMetrics aMetrics) {
+ return (isResizing() ? mHeightDuringResize : aMetrics.getHeight())
+ + mMaxTranslation - mLayerViewTranslation;
+ }
+
+ private synchronized boolean getAndClearSnapRequired() {
+ boolean snapRequired = mSnapRequired;
+ mSnapRequired = false;
+ return snapRequired;
+ }
+
+ void populateViewTransform(ViewTransform aTransform, ImmutableViewportMetrics aMetrics) {
+ if (getAndClearSnapRequired()) {
+ synchronized (mSnapRunnable) {
+ ViewCompat.postOnAnimation(mTarget.getView(), mSnapRunnable);
+ try {
+ // hold the in-progress composite until the views have been
+ // translated because otherwise there is a visible glitch.
+ // don't hold for more than 100ms just in case.
+ mSnapRunnable.wait(100);
+ } catch (InterruptedException ie) {
+ }
+ }
+ }
+
+ aTransform.x = aMetrics.viewportRectLeft;
+ aTransform.y = aMetrics.viewportRectTop;
+ aTransform.width = aMetrics.viewportRectWidth;
+ aTransform.height = aMetrics.viewportRectHeight;
+ aTransform.scale = aMetrics.zoomFactor;
+
+ aTransform.fixedLayerMarginTop = mLayerViewTranslation - mToolbarTranslation;
+ float bottomOfScreen = mTarget.getView().getHeight();
+ // We want to move a fixed item from "bottomOfCssViewport" to
+ // "bottomOfScreen". But also the bottom margin > 0 means that bottom
+ // fixed-pos items will move upwards.
+ aTransform.fixedLayerMarginBottom = bottomOfCssViewport(aMetrics) - bottomOfScreen;
+ //Log.v(LOGTAG, "ViewTransform is x=" + aTransform.x + " y=" + aTransform.y
+ // + " z=" + aTransform.scale + " t=" + aTransform.fixedLayerMarginTop
+ // + " b=" + aTransform.fixedLayerMarginBottom);
+ }
+
+ class DynamicToolbarAnimationTask extends RenderTask {
+ private final float mStartTranslation;
+ private final float mEndTranslation;
+ private final boolean mImmediate;
+ private final boolean mShiftLayerView;
+ private boolean mContinueAnimation;
+
+ public DynamicToolbarAnimationTask(float aTranslation, boolean aImmediate, boolean aShiftLayerView) {
+ super(false);
+ mContinueAnimation = true;
+ mStartTranslation = mToolbarTranslation;
+ mEndTranslation = aTranslation;
+ mImmediate = aImmediate;
+ mShiftLayerView = aShiftLayerView;
+ }
+
+ float getFinalToolbarTranslation() {
+ return mEndTranslation;
+ }
+
+ @Override
+ public boolean internalRun(long timeDelta, long currentFrameStartTime) {
+ if (!mContinueAnimation) {
+ return false;
+ }
+
+ // Calculate the progress (between 0 and 1)
+ final float progress = mImmediate
+ ? 1.0f
+ : mInterpolator.getInterpolation(
+ Math.min(1.0f, (System.nanoTime() - getStartTime())
+ / (float)ANIMATION_DURATION));
+
+ // This runs on the compositor thread, so we need to post the
+ // actual work to the UI thread.
+ ThreadUtils.assertNotOnUiThread();
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Move the toolbar as per the animation
+ mToolbarTranslation = FloatUtils.interpolate(mStartTranslation, mEndTranslation, progress);
+ fireListeners();
+
+ if (mShiftLayerView && progress >= 1.0f) {
+ shiftLayerView(mEndTranslation);
+ }
+ }
+ });
+
+ mTarget.getView().requestRender();
+ if (progress >= 1.0f) {
+ mContinueAnimation = false;
+ }
+ return mContinueAnimation;
+ }
+ }
+
+ class SnapMetrics {
+ public final int viewportWidth;
+ public final int viewportHeight;
+ public final float scrollChangeY;
+
+ SnapMetrics(ImmutableViewportMetrics aMetrics, float aScrollChange) {
+ viewportWidth = aMetrics.viewportRectWidth;
+ viewportHeight = aMetrics.viewportRectHeight;
+ scrollChangeY = aScrollChange;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java
@@ -0,0 +1,54 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class FloatSize {
+ public final float width, height;
+
+ public FloatSize(FloatSize size) { width = size.width; height = size.height; }
+ public FloatSize(IntSize size) { width = size.width; height = size.height; }
+ public FloatSize(float aWidth, float aHeight) { width = aWidth; height = aHeight; }
+
+ public FloatSize(JSONObject json) {
+ try {
+ width = (float)json.getDouble("width");
+ height = (float)json.getDouble("height");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String toString() { return "(" + width + "," + height + ")"; }
+
+ public boolean isPositive() {
+ return (width > 0 && height > 0);
+ }
+
+ public boolean fuzzyEquals(FloatSize size) {
+ return (FloatUtils.fuzzyEquals(size.width, width) &&
+ FloatUtils.fuzzyEquals(size.height, height));
+ }
+
+ public FloatSize scale(float factor) {
+ return new FloatSize(width * factor, height * factor);
+ }
+
+ /*
+ * Returns the size that represents a linear transition between this size and `to` at time `t`,
+ * which is on the scale [0, 1).
+ */
+ public FloatSize interpolate(FloatSize to, float t) {
+ return new FloatSize(FloatUtils.interpolate(width, to.width, t),
+ FloatUtils.interpolate(height, to.height, t));
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java
@@ -0,0 +1,12 @@
+/* -*- 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.gfx;
+
+public enum FullScreenState {
+ NONE,
+ ROOT_ELEMENT,
+ NON_ROOT_ELEMENT
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GLController.java
@@ -0,0 +1,183 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.util.Log;
+
+/**
+ * This class is a singleton that tracks EGL and compositor things over
+ * the lifetime of Fennec running.
+ * We only ever create one C++ compositor over Fennec's lifetime, but
+ * most of the Java-side objects (e.g. LayerView, GeckoLayerClient,
+ * LayerRenderer) can all get destroyed and re-created if the GeckoApp
+ * activity is destroyed. This GLController is never destroyed, so that
+ * the mCompositorCreated field and other state variables are always
+ * accurate.
+ */
+public class GLController extends JNIObject {
+ private static final String LOGTAG = "GeckoGLController";
+
+ /* package */ LayerView mView;
+ private boolean mServerSurfaceValid;
+ private int mWidth, mHeight;
+
+ /* This is written by the compositor thread (while the UI thread
+ * is blocked on it) and read by the UI thread. */
+ private volatile boolean mCompositorCreated;
+
+ @WrapForJNI @Override // JNIObject
+ protected native void disposeNative();
+
+ // Gecko thread sets its Java instances; does not block UI thread.
+ @WrapForJNI
+ /* package */ native void attachToJava(GeckoLayerClient layerClient,
+ NativePanZoomController npzc);
+
+ @WrapForJNI
+ /* package */ native void onSizeChanged(int windowWidth, int windowHeight,
+ int screenWidth, int screenHeight);
+
+ // Gecko thread creates compositor; blocks UI thread.
+ @WrapForJNI
+ private native void createCompositor(int width, int height);
+
+ // Gecko thread pauses compositor; blocks UI thread.
+ @WrapForJNI
+ private native void pauseCompositor();
+
+ // UI thread resumes compositor and notifies Gecko thread; does not block UI thread.
+ @WrapForJNI
+ private native void syncResumeResizeCompositor(int width, int height);
+
+ @WrapForJNI
+ private native void syncInvalidateAndScheduleComposite();
+
+ public GLController() {
+ }
+
+ void serverSurfaceDestroyed() {
+ ThreadUtils.assertOnUiThread();
+
+ // We need to coordinate with Gecko when pausing composition, to ensure
+ // that Gecko never executes a draw event while the compositor is paused.
+ // This is sent synchronously to make sure that we don't attempt to use
+ // any outstanding Surfaces after we call this (such as from a
+ // serverSurfaceDestroyed notification), and to make sure that any in-flight
+ // Gecko draw events have been processed. When this returns, composition is
+ // definitely paused -- it'll synchronize with the Gecko event loop, which
+ // in turn will synchronize with the compositor thread.
+ if (mCompositorCreated) {
+ pauseCompositor();
+ }
+
+ synchronized (this) {
+ mServerSurfaceValid = false;
+ }
+ }
+
+ void serverSurfaceChanged(int newWidth, int newHeight) {
+ ThreadUtils.assertOnUiThread();
+
+ synchronized (this) {
+ mWidth = newWidth;
+ mHeight = newHeight;
+ mServerSurfaceValid = true;
+ }
+
+ updateCompositor();
+ }
+
+ void updateCompositor() {
+ ThreadUtils.assertOnUiThread();
+
+ if (mView == null) {
+ return;
+ }
+
+ if (mCompositorCreated) {
+ // If the compositor has already been created, just resume it instead. We don't need
+ // to block here because if the surface is destroyed before the compositor grabs it,
+ // we can handle that gracefully (i.e. the compositor will remain paused).
+ resumeCompositor(mWidth, mHeight);
+ return;
+ }
+
+ // Only try to create the compositor if we have a valid surface and gecko is up. When these
+ // two conditions are satisfied, we can be relatively sure that the compositor creation will
+ // happen without needing to block anywhere. Do it with a synchronous Gecko event so that the
+ // Android doesn't have a chance to destroy our surface in between.
+ if (isServerSurfaceValid() && mView.getLayerClient().isGeckoReady()) {
+ createCompositor(mWidth, mHeight);
+ compositorCreated();
+ }
+ }
+
+ void compositorCreated() {
+ // This is invoked on the compositor thread, while the java UI thread
+ // is blocked on the gecko sync event in updateCompositor() above
+ mCompositorCreated = true;
+ }
+
+ public boolean isServerSurfaceValid() {
+ return mServerSurfaceValid;
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ private synchronized Object getSurface() {
+ if (mView != null && isServerSurfaceValid()) {
+ return mView.getSurface();
+ } else {
+ return null;
+ }
+ }
+
+ void resumeCompositor(int width, int height) {
+ // Asking Gecko to resume the compositor takes too long (see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
+ // resume the compositor directly. We still need to inform Gecko about
+ // the compositor resuming, so that Gecko knows that it can now draw.
+ // It is important to not notify Gecko until after the compositor has
+ // been resumed, otherwise Gecko may send updates that get dropped.
+ if (isServerSurfaceValid() && mCompositorCreated) {
+ syncResumeResizeCompositor(width, height);
+ mView.requestRender();
+ }
+ }
+
+ /* package */ void invalidateAndScheduleComposite() {
+ if (mCompositorCreated) {
+ syncInvalidateAndScheduleComposite();
+ }
+ }
+
+ @WrapForJNI
+ private void destroy() {
+ // The nsWindow has been closed. First mark our compositor as destroyed.
+ mCompositorCreated = false;
+
+ // Then clear out any pending calls on the UI thread by disposing on the UI thread.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GLController.this.disposeNative();
+ }
+ });
+ }
+
+ public static class GLControllerException extends RuntimeException {
+ public static final long serialVersionUID = 1L;
+
+ GLControllerException(String e) {
+ super(e);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -0,0 +1,1132 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.gfx.LayerView.DrawListener;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.AppConstants;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
+{
+ private static final String LOGTAG = "GeckoLayerClient";
+ private static int sPaintSyncId = 1;
+
+ private LayerRenderer mLayerRenderer;
+ private boolean mLayerRendererInitialized;
+
+ private final Context mContext;
+ private IntSize mScreenSize;
+ private IntSize mWindowSize;
+ private DisplayPortMetrics mDisplayPort;
+
+ private boolean mRecordDrawTimes;
+ private final DrawTimingQueue mDrawTimingQueue;
+
+ private VirtualLayer mRootLayer;
+
+ /* The Gecko viewport as per the UI thread. Must be touched only on the UI thread.
+ * If any events being sent to Gecko that are relative to the Gecko viewport position,
+ * they must (a) be relative to this viewport, and (b) be sent on the UI thread to
+ * avoid races. As long as these two conditions are satisfied, and the events being
+ * sent to Gecko are processed in FIFO order, the events will properly be relative
+ * to the Gecko viewport position. Note that if Gecko updates its viewport independently,
+ * we get notified synchronously and also update this on the UI thread.
+ */
+ private ImmutableViewportMetrics mGeckoViewport;
+
+ /*
+ * The viewport metrics being used to draw the current frame. This is only
+ * accessed by the compositor thread, and so needs no synchronisation.
+ */
+ private ImmutableViewportMetrics mFrameMetrics;
+
+ private final List<DrawListener> mDrawListeners;
+
+ /* Used as temporaries by syncViewportInfo */
+ private final ViewTransform mCurrentViewTransform;
+
+ /* Used as the return value of progressiveUpdateCallback */
+ private final ProgressiveUpdateData mProgressiveUpdateData;
+ private DisplayPortMetrics mProgressiveUpdateDisplayPort;
+ private boolean mLastProgressiveUpdateWasLowPrecision;
+ private boolean mProgressiveUpdateWasInDanger;
+
+ private boolean mForceRedraw;
+
+ /* The current viewport metrics.
+ * This is volatile so that we can read and write to it from different threads.
+ * We avoid synchronization to make getting the viewport metrics from
+ * the compositor as cheap as possible. The viewport is immutable so
+ * we don't need to worry about anyone mutating it while we're reading from it.
+ * Specifically:
+ * 1) reading mViewportMetrics from any thread is fine without synchronization
+ * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
+ * 3) whenever reading multiple fields from mViewportMetrics without synchronization (i.e. in
+ * case 1 above) you should always first grab a local copy of the reference, and then use
+ * that because mViewportMetrics might get reassigned in between reading the different
+ * fields. */
+ private volatile ImmutableViewportMetrics mViewportMetrics;
+
+ private volatile boolean mGeckoIsReady;
+
+ private final PanZoomController mPanZoomController;
+ private final DynamicToolbarAnimator mToolbarAnimator;
+ private final LayerView mView;
+
+ /* This flag is true from the time that browser.js detects a first-paint is about to start,
+ * to the time that we receive the first-paint composite notification from the compositor.
+ * Note that there is a small race condition with this; if there are two paints that both
+ * have the first-paint flag set, and the second paint happens concurrently with the
+ * composite for the first paint, then this flag may be set to true prematurely. Fixing this
+ * is possible but risky; see https://bugzilla.mozilla.org/show_bug.cgi?id=797615#c751
+ */
+ private volatile boolean mContentDocumentIsDisplayed;
+
+ private SynthesizedEventState mPointerState;
+
+ public GeckoLayerClient(Context context, LayerView view, EventDispatcher eventDispatcher) {
+ // we can fill these in with dummy values because they are always written
+ // to before being read
+ mContext = context;
+ mScreenSize = new IntSize(0, 0);
+ mWindowSize = new IntSize(0, 0);
+ mDisplayPort = new DisplayPortMetrics();
+ mRecordDrawTimes = true;
+ mDrawTimingQueue = new DrawTimingQueue();
+ mCurrentViewTransform = new ViewTransform(0, 0, 1);
+ mProgressiveUpdateData = new ProgressiveUpdateData();
+ mProgressiveUpdateDisplayPort = new DisplayPortMetrics();
+
+ mForceRedraw = true;
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ mViewportMetrics = new ImmutableViewportMetrics(displayMetrics)
+ .setViewportSize(view.getWidth(), view.getHeight());
+
+ mFrameMetrics = mViewportMetrics;
+
+ mDrawListeners = new ArrayList<DrawListener>();
+ mToolbarAnimator = new DynamicToolbarAnimator(this);
+ mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher);
+ mView = view;
+ mView.setListener(this);
+ mContentDocumentIsDisplayed = true;
+ }
+
+ public void setOverscrollHandler(final Overscroll listener) {
+ mPanZoomController.setOverscrollHandler(listener);
+ }
+
+ /** Attaches to root layer so that Gecko appears. */
+ /* package */ boolean isGeckoReady() {
+ return mGeckoIsReady;
+ }
+
+ @WrapForJNI
+ private void onGeckoReady() {
+ mGeckoIsReady = true;
+
+ mRootLayer = new VirtualLayer(new IntSize(mView.getWidth(), mView.getHeight()));
+ mLayerRenderer = mView.getRenderer();
+
+ sendResizeEventIfNecessary(true, null);
+
+ DisplayPortCalculator.initPrefs();
+
+ // Gecko being ready is one of the two conditions (along with having an available
+ // surface) that cause us to create the compositor. So here, now that we know gecko
+ // is ready, call updateCompositor() to see if we can actually do the creation.
+ // This needs to run on the UI thread so that the surface validity can't change on
+ // us while we're in the middle of creating the compositor.
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.getGLController().updateCompositor();
+ }
+ });
+ }
+
+ public void destroy() {
+ mPanZoomController.destroy();
+ mToolbarAnimator.destroy();
+ mDrawListeners.clear();
+ }
+
+ /**
+ * Returns true if this client is fine with performing a redraw operation or false if it
+ * would prefer that the action didn't take place.
+ */
+ private boolean getRedrawHint() {
+ if (mForceRedraw) {
+ mForceRedraw = false;
+ return true;
+ }
+
+ if (!mPanZoomController.getRedrawHint()) {
+ return false;
+ }
+
+ return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics,
+ mPanZoomController.getVelocityVector(), mDisplayPort);
+ }
+
+ Layer getRoot() {
+ return mGeckoIsReady ? mRootLayer : null;
+ }
+
+ public LayerView getView() {
+ return mView;
+ }
+
+ public FloatSize getViewportSize() {
+ return mViewportMetrics.getSize();
+ }
+
+ /**
+ * The view calls this function to indicate that the viewport changed size. It must hold the
+ * monitor while calling it.
+ *
+ * TODO: Refactor this to use an interface. Expose that interface only to the view and not
+ * to the layer client. That way, the layer client won't be tempted to call this, which might
+ * result in an infinite loop.
+ */
+ boolean setViewportSize(int width, int height, PointF scrollChange) {
+ if (mViewportMetrics.viewportRectWidth == width &&
+ mViewportMetrics.viewportRectHeight == height &&
+ (scrollChange == null || (scrollChange.x == 0 && scrollChange.y == 0))) {
+ return false;
+ }
+ mViewportMetrics = mViewportMetrics.setViewportSize(width, height);
+ if (scrollChange != null) {
+ mViewportMetrics = mPanZoomController.adjustScrollForSurfaceShift(mViewportMetrics, scrollChange);
+ }
+
+ if (mGeckoIsReady) {
+ // here we send gecko a resize message. The code in browser.js is responsible for
+ // picking up on that resize event, modifying the viewport as necessary, and informing
+ // us of the new viewport.
+ sendResizeEventIfNecessary(true, scrollChange);
+
+ // the following call also sends gecko a message, which will be processed after the resize
+ // message above has updated the viewport. this message ensures that if we have just put
+ // focus in a text field, we scroll the content so that the text field is in view.
+ GeckoAppShell.viewSizeChanged();
+ }
+ return true;
+ }
+
+ PanZoomController getPanZoomController() {
+ return mPanZoomController;
+ }
+
+ DynamicToolbarAnimator getDynamicToolbarAnimator() {
+ return mToolbarAnimator;
+ }
+
+ /* Informs Gecko that the screen size has changed. */
+ private void sendResizeEventIfNecessary(boolean force, PointF scrollChange) {
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+
+ IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
+ IntSize newWindowSize = new IntSize(mViewportMetrics.viewportRectWidth,
+ mViewportMetrics.viewportRectHeight);
+
+ boolean screenSizeChanged = !mScreenSize.equals(newScreenSize);
+ boolean windowSizeChanged = !mWindowSize.equals(newWindowSize);
+
+ if (!force && !screenSizeChanged && !windowSizeChanged) {
+ return;
+ }
+
+ mScreenSize = newScreenSize;
+ mWindowSize = newWindowSize;
+
+ if (screenSizeChanged) {
+ Log.d(LOGTAG, "Screen-size changed to " + mScreenSize);
+ }
+
+ if (windowSizeChanged) {
+ Log.d(LOGTAG, "Window-size changed to " + mWindowSize);
+ }
+
+ if (mView != null) {
+ mView.getGLController().onSizeChanged(mWindowSize.width, mWindowSize.height,
+ mScreenSize.width, mScreenSize.height);
+ }
+
+ String json = "";
+ try {
+ if (scrollChange != null) {
+ int id = ++sPaintSyncId;
+ if (id == 0) {
+ // never use 0 as that is the default value for "this is not
+ // a special transaction"
+ id = ++sPaintSyncId;
+ }
+ JSONObject jsonObj = new JSONObject();
+ jsonObj.put("x", scrollChange.x / mViewportMetrics.zoomFactor);
+ jsonObj.put("y", scrollChange.y / mViewportMetrics.zoomFactor);
+ jsonObj.put("id", id);
+ json = jsonObj.toString();
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Unable to convert point to JSON", e);
+ }
+ GeckoAppShell.notifyObservers("Window:Resize", json);
+ }
+
+ /** Sets the current page rect. You must hold the monitor while calling this. */
+ private void setPageRect(RectF rect, RectF cssRect) {
+ // Since the "rect" is always just a multiple of "cssRect" we don't need to
+ // check both; this function assumes that both "rect" and "cssRect" are relative
+ // the zoom factor in mViewportMetrics.
+ if (mViewportMetrics.getCssPageRect().equals(cssRect))
+ return;
+
+ mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect);
+
+ // Page size is owned by the layer client, so no need to notify it of
+ // this change.
+
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mPanZoomController.pageRectUpdated();
+ mView.requestRender();
+ }
+ });
+ }
+
+ private void adjustViewport(DisplayPortMetrics displayPort) {
+ // TODO: APZ For fennec might need margins information to deal with
+ // the dynamic toolbar.
+ if (AppConstants.MOZ_ANDROID_APZ)
+ return;
+
+ ImmutableViewportMetrics metrics = getViewportMetrics();
+ ImmutableViewportMetrics clampedMetrics = metrics.clamp();
+
+ if (displayPort == null) {
+ displayPort = DisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector());
+ }
+
+ mDisplayPort = displayPort;
+ mGeckoViewport = clampedMetrics;
+
+ if (mRecordDrawTimes) {
+ mDrawTimingQueue.add(displayPort);
+ }
+
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createViewportEvent(clampedMetrics, displayPort));
+ }
+
+ /** Aborts any pan/zoom animation that is currently in progress. */
+ private void abortPanZoomAnimation() {
+ if (mPanZoomController != null) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mPanZoomController.abortAnimation();
+ }
+ });
+ }
+ }
+
+ /**
+ * The different types of Viewport messages handled. All viewport events
+ * expect a display-port to be returned, but can handle one not being
+ * returned.
+ */
+ private enum ViewportMessageType {
+ UPDATE, // The viewport has changed and should be entirely updated
+ PAGE_SIZE // The viewport's page-size has changed
+ }
+
+ /** Viewport message handler. */
+ private DisplayPortMetrics handleViewportMessage(ImmutableViewportMetrics messageMetrics, ViewportMessageType type) {
+ synchronized (getLock()) {
+ ImmutableViewportMetrics newMetrics;
+ ImmutableViewportMetrics oldMetrics = getViewportMetrics();
+
+ switch (type) {
+ default:
+ case UPDATE:
+ // Keep the old viewport size
+ newMetrics = messageMetrics.setViewportSize(oldMetrics.viewportRectWidth, oldMetrics.viewportRectHeight);
+ if (mToolbarAnimator.isResizing()) {
+ // If we're in the middle of a resize, we don't want to clobber
+ // the scroll offset, so grab the one from the oldMetrics and
+ // keep using that. We also don't want to abort animations,
+ // because at that point we're guaranteed to not be animating
+ // anyway, and calling abortPanZoomAnimation has a nasty
+ // side-effect of clmaping and clobbering the metrics, which
+ // we don't want here.
+ newMetrics = newMetrics.setViewportOrigin(oldMetrics.viewportRectLeft, oldMetrics.viewportRectTop);
+ break;
+ }
+ if (!oldMetrics.fuzzyEquals(newMetrics)) {
+ abortPanZoomAnimation();
+ }
+ break;
+ case PAGE_SIZE:
+ // adjust the page dimensions to account for differences in zoom
+ // between the rendered content (which is what Gecko tells us)
+ // and our zoom level (which may have diverged).
+ float scaleFactor = oldMetrics.zoomFactor / messageMetrics.zoomFactor;
+ newMetrics = oldMetrics.setPageRect(RectUtils.scale(messageMetrics.getPageRect(), scaleFactor), messageMetrics.getCssPageRect());
+ break;
+ }
+
+ // Update the Gecko-side viewport metrics. Make sure to do this
+ // before modifying the metrics below.
+ final ImmutableViewportMetrics geckoMetrics = newMetrics.clamp();
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mGeckoViewport = geckoMetrics;
+ }
+ });
+
+ setViewportMetrics(newMetrics, type == ViewportMessageType.UPDATE);
+ mDisplayPort = DisplayPortCalculator.calculate(getViewportMetrics(), null);
+ }
+ return mDisplayPort;
+ }
+
+ @WrapForJNI
+ DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) {
+ return null;
+ }
+
+ @WrapForJNI
+ void contentDocumentChanged() {
+ mContentDocumentIsDisplayed = false;
+ }
+
+ @WrapForJNI
+ boolean isContentDocumentDisplayed() {
+ return mContentDocumentIsDisplayed;
+ }
+
+ // This is called on the Gecko thread to determine if we're still interested
+ // in the update of this display-port to continue. We can return true here
+ // to abort the current update and continue with any subsequent ones. This
+ // is useful for slow-to-render pages when the display-port starts lagging
+ // behind enough that continuing to draw it is wasted effort.
+ @WrapForJNI(allowMultithread = true)
+ public ProgressiveUpdateData progressiveUpdateCallback(boolean aHasPendingNewThebesContent,
+ float x, float y, float width, float height,
+ float resolution, boolean lowPrecision) {
+ // Reset the checkerboard risk flag when switching to low precision
+ // rendering.
+ if (lowPrecision && !mLastProgressiveUpdateWasLowPrecision) {
+ // Skip low precision rendering until we're at risk of checkerboarding.
+ if (!mProgressiveUpdateWasInDanger) {
+ mProgressiveUpdateData.abort = true;
+ return mProgressiveUpdateData;
+ }
+ mProgressiveUpdateWasInDanger = false;
+ }
+ mLastProgressiveUpdateWasLowPrecision = lowPrecision;
+
+ // Grab a local copy of the last display-port sent to Gecko and the
+ // current viewport metrics to avoid races when accessing them.
+ DisplayPortMetrics displayPort = mDisplayPort;
+ ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+ mProgressiveUpdateData.setViewport(viewportMetrics);
+ mProgressiveUpdateData.abort = false;
+
+ // Always abort updates if the resolution has changed. There's no use
+ // in drawing at the incorrect resolution.
+ if (!FloatUtils.fuzzyEquals(resolution, viewportMetrics.zoomFactor)) {
+ Log.d(LOGTAG, "Aborting draw due to resolution change: " + resolution + " != " + viewportMetrics.zoomFactor);
+ mProgressiveUpdateData.abort = true;
+ return mProgressiveUpdateData;
+ }
+
+ // Store the high precision displayport for comparison when doing low
+ // precision updates.
+ if (!lowPrecision) {
+ if (!FloatUtils.fuzzyEquals(resolution, mProgressiveUpdateDisplayPort.resolution) ||
+ !FloatUtils.fuzzyEquals(x, mProgressiveUpdateDisplayPort.getLeft()) ||
+ !FloatUtils.fuzzyEquals(y, mProgressiveUpdateDisplayPort.getTop()) ||
+ !FloatUtils.fuzzyEquals(x + width, mProgressiveUpdateDisplayPort.getRight()) ||
+ !FloatUtils.fuzzyEquals(y + height, mProgressiveUpdateDisplayPort.getBottom())) {
+ mProgressiveUpdateDisplayPort =
+ new DisplayPortMetrics(x, y, x + width, y + height, resolution);
+ }
+ }
+
+ // If we're not doing low precision draws and we're about to
+ // checkerboard, enable low precision drawing.
+ if (!lowPrecision && !mProgressiveUpdateWasInDanger) {
+ if (DisplayPortCalculator.aboutToCheckerboard(viewportMetrics,
+ mPanZoomController.getVelocityVector(), mProgressiveUpdateDisplayPort)) {
+ mProgressiveUpdateWasInDanger = true;
+ }
+ }
+
+ // XXX All sorts of rounding happens inside Gecko that becomes hard to
+ // account exactly for. Given we align the display-port to tile
+ // boundaries (and so they rarely vary by sub-pixel amounts), just
+ // check that values are within a couple of pixels of the
+ // display-port bounds.
+
+ // Never abort drawing if we can't be sure we've sent a more recent
+ // display-port. If we abort updating when we shouldn't, we can end up
+ // with blank regions on the screen and we open up the risk of entering
+ // an endless updating cycle.
+ if (Math.abs(displayPort.getLeft() - mProgressiveUpdateDisplayPort.getLeft()) <= 2 &&
+ Math.abs(displayPort.getTop() - mProgressiveUpdateDisplayPort.getTop()) <= 2 &&
+ Math.abs(displayPort.getBottom() - mProgressiveUpdateDisplayPort.getBottom()) <= 2 &&
+ Math.abs(displayPort.getRight() - mProgressiveUpdateDisplayPort.getRight()) <= 2) {
+ return mProgressiveUpdateData;
+ }
+
+ // Abort updates when the display-port no longer contains the visible
+ // area of the page (that is, the viewport cropped by the page
+ // boundaries).
+ // XXX This makes the assumption that we never let the visible area of
+ // the page fall outside of the display-port.
+ if (Math.max(viewportMetrics.viewportRectLeft, viewportMetrics.pageRectLeft) + 1 < x ||
+ Math.max(viewportMetrics.viewportRectTop, viewportMetrics.pageRectTop) + 1 < y ||
+ Math.min(viewportMetrics.viewportRectRight(), viewportMetrics.pageRectRight) - 1 > x + width ||
+ Math.min(viewportMetrics.viewportRectBottom(), viewportMetrics.pageRectBottom) - 1 > y + height) {
+ Log.d(LOGTAG, "Aborting update due to viewport not in display-port");
+ mProgressiveUpdateData.abort = true;
+
+ // Enable low-precision drawing, as we're likely to be in danger if
+ // this situation has been encountered.
+ mProgressiveUpdateWasInDanger = true;
+
+ return mProgressiveUpdateData;
+ }
+
+ // Abort drawing stale low-precision content if there's a more recent
+ // display-port in the pipeline.
+ if (lowPrecision && !aHasPendingNewThebesContent) {
+ mProgressiveUpdateData.abort = true;
+ }
+ return mProgressiveUpdateData;
+ }
+
+ /** The compositor invokes this function just before compositing a frame where the document
+ * is different from the document composited on the last frame. In these cases, the viewport
+ * information we have in Java is no longer valid and needs to be replaced with the new
+ * viewport information provided. setPageRect will never be invoked on the same frame that
+ * this function is invoked on; and this function will always be called prior to syncViewportInfo.
+ */
+ @WrapForJNI(allowMultithread = true)
+ public void setFirstPaintViewport(float offsetX, float offsetY, float zoom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
+ synchronized (getLock()) {
+ ImmutableViewportMetrics currentMetrics = getViewportMetrics();
+
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ RectF pageRect = RectUtils.scaleAndRound(cssPageRect, zoom);
+
+ final ImmutableViewportMetrics newMetrics = currentMetrics
+ .setViewportOrigin(offsetX, offsetY)
+ .setZoomFactor(zoom)
+ .setPageRect(pageRect, cssPageRect);
+ // Since we have switched to displaying a different document, we need to update any
+ // viewport-related state we have lying around. This includes mGeckoViewport and
+ // mViewportMetrics. Usually this information is updated via handleViewportMessage
+ // while we remain on the same document.
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mGeckoViewport = newMetrics;
+ }
+ });
+
+ setViewportMetrics(newMetrics);
+
+ // At this point, we have just switched to displaying a different document than we
+ // we previously displaying. This means we need to abort any panning/zooming animations
+ // that are in progress and send an updated display port request to browser.js as soon
+ // as possible. The call to PanZoomController.abortAnimation accomplishes this by calling the
+ // forceRedraw function, which sends the viewport to gecko. The display port request is
+ // actually a full viewport update, which is fine because if browser.js has somehow moved to
+ // be out of sync with this first-paint viewport, then we force them back in sync.
+ abortPanZoomAnimation();
+
+ // Indicate that the document is about to be composited so the
+ // LayerView background can be removed.
+ if (mView.getPaintState() == LayerView.PAINT_START) {
+ mView.setPaintState(LayerView.PAINT_BEFORE_FIRST);
+ }
+ }
+ DisplayPortCalculator.resetPageState();
+ mDrawTimingQueue.reset();
+
+ mContentDocumentIsDisplayed = true;
+ }
+
+ /** The compositor invokes this function whenever it determines that the page rect
+ * has changed (based on the information it gets from layout). If setFirstPaintViewport
+ * is invoked on a frame, then this function will not be. For any given frame, this
+ * function will be invoked before syncViewportInfo.
+ */
+ @WrapForJNI(allowMultithread = true)
+ public void setPageRect(float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
+ synchronized (getLock()) {
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ float ourZoom = getViewportMetrics().zoomFactor;
+ setPageRect(RectUtils.scale(cssPageRect, ourZoom), cssPageRect);
+ // Here the page size of the document has changed, but the document being displayed
+ // is still the same. Therefore, we don't need to send anything to browser.js; any
+ // changes we need to make to the display port will get sent the next time we call
+ // adjustViewport().
+ }
+ }
+
+ /** The compositor invokes this function on every frame to figure out what part of the
+ * page to display, and to inform Java of the current display port. Since it is called
+ * on every frame, it needs to be ultra-fast.
+ * It avoids taking any locks or allocating any objects. We keep around a
+ * mCurrentViewTransform so we don't need to allocate a new ViewTransform
+ * every time we're called. NOTE: we might be able to return a ImmutableViewportMetrics
+ * which would avoid the copy into mCurrentViewTransform.
+ */
+ @WrapForJNI(allowMultithread = true)
+ public ViewTransform syncViewportInfo(int x, int y, int width, int height, float resolution, boolean layersUpdated,
+ int paintSyncId) {
+ // getViewportMetrics is thread safe so we don't need to synchronize.
+ // We save the viewport metrics here, so we later use it later in
+ // createFrame (which will be called by nsWindow::DrawWindowUnderlay on
+ // the native side, by the compositor). The viewport
+ // metrics can change between here and there, as it's accessed outside
+ // of the compositor thread.
+ mFrameMetrics = getViewportMetrics();
+
+ if (paintSyncId == sPaintSyncId) {
+ mToolbarAnimator.scrollChangeResizeCompleted();
+ }
+ mToolbarAnimator.populateViewTransform(mCurrentViewTransform, mFrameMetrics);
+
+ if (mRootLayer != null) {
+ mRootLayer.setPositionAndResolution(
+ x, y, x + width, y + height,
+ resolution);
+ }
+
+ if (layersUpdated && mRecordDrawTimes) {
+ // If we got a layers update, that means a draw finished. Check to see if the area drawn matches
+ // one of our requested displayports; if it does calculate the draw time and notify the
+ // DisplayPortCalculator
+ DisplayPortMetrics drawn = new DisplayPortMetrics(x, y, x + width, y + height, resolution);
+ long time = mDrawTimingQueue.findTimeFor(drawn);
+ if (time >= 0) {
+ long now = SystemClock.uptimeMillis();
+ time = now - time;
+ mRecordDrawTimes = DisplayPortCalculator.drawTimeUpdate(time, width * height);
+ }
+ }
+
+ if (layersUpdated) {
+ for (DrawListener listener : mDrawListeners) {
+ listener.drawFinished();
+ }
+ }
+
+ return mCurrentViewTransform;
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public ViewTransform syncFrameMetrics(float scrollX, float scrollY, float zoom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom,
+ int dpX, int dpY, int dpWidth, int dpHeight, float paintedResolution,
+ boolean layersUpdated, int paintSyncId)
+ {
+ // TODO: optimize this so it doesn't create so much garbage - it's a
+ // hot path
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ synchronized (getLock()) {
+ mViewportMetrics = mViewportMetrics.setViewportOrigin(scrollX, scrollY)
+ .setZoomFactor(zoom)
+ .setPageRect(RectUtils.scale(cssPageRect, zoom), cssPageRect);
+ }
+ return syncViewportInfo(dpX, dpY, dpWidth, dpHeight, paintedResolution,
+ layersUpdated, paintSyncId);
+ }
+
+ class PointerInfo {
+ // We reserve one pointer ID for the mouse, so that tests don't have
+ // to worry about tracking pointer IDs if they just want to test mouse
+ // event synthesization. If somebody tries to use this ID for a
+ // synthesized touch event we'll throw an exception.
+ public static final int RESERVED_MOUSE_POINTER_ID = 100000;
+
+ public int pointerId;
+ public int source;
+ public int screenX;
+ public int screenY;
+ public double pressure;
+ public int orientation;
+
+ public MotionEvent.PointerCoords getCoords() {
+ MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ coords.orientation = orientation;
+ coords.pressure = (float)pressure;
+ coords.x = screenX;
+ coords.y = screenY;
+ return coords;
+ }
+ }
+
+ class SynthesizedEventState {
+ public final ArrayList<PointerInfo> pointers;
+ public long downTime;
+
+ SynthesizedEventState() {
+ pointers = new ArrayList<PointerInfo>();
+ }
+
+ int getPointerIndex(int pointerId) {
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).pointerId == pointerId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ int addPointer(int pointerId, int source) {
+ PointerInfo info = new PointerInfo();
+ info.pointerId = pointerId;
+ info.source = source;
+ pointers.add(info);
+ return pointers.size() - 1;
+ }
+
+ int getPointerCount(int source) {
+ int count = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ MotionEvent.PointerProperties[] getPointerProperties(int source) {
+ MotionEvent.PointerProperties[] props = new MotionEvent.PointerProperties[getPointerCount(source)];
+ int index = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ MotionEvent.PointerProperties p = new MotionEvent.PointerProperties();
+ p.id = pointers.get(i).pointerId;
+ switch (source) {
+ case InputDevice.SOURCE_TOUCHSCREEN:
+ p.toolType = MotionEvent.TOOL_TYPE_FINGER;
+ break;
+ case InputDevice.SOURCE_MOUSE:
+ p.toolType = MotionEvent.TOOL_TYPE_MOUSE;
+ break;
+ }
+ props[index++] = p;
+ }
+ }
+ return props;
+ }
+
+ MotionEvent.PointerCoords[] getPointerCoords(int source) {
+ MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[getPointerCount(source)];
+ int index = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ coords[index++] = pointers.get(i).getCoords();
+ }
+ }
+ return coords;
+ }
+ }
+
+ private void synthesizeNativePointer(int source, int pointerId,
+ int eventType, int screenX, int screenY, double pressure,
+ int orientation)
+ {
+ Log.d(LOGTAG, "Synthesizing pointer from " + source + " id " + pointerId + " at " + screenX + ", " + screenY);
+
+ if (mPointerState == null) {
+ mPointerState = new SynthesizedEventState();
+ }
+
+ // Find the pointer if it already exists
+ int pointerIndex = mPointerState.getPointerIndex(pointerId);
+
+ // Event-specific handling
+ switch (eventType) {
+ case MotionEvent.ACTION_POINTER_UP:
+ if (pointerIndex < 0) {
+ Log.d(LOGTAG, "Requested synthesis of a pointer-up for a pointer that doesn't exist!");
+ return;
+ }
+ if (mPointerState.pointers.size() == 1) {
+ // Last pointer is going up
+ eventType = MotionEvent.ACTION_UP;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (pointerIndex < 0) {
+ Log.d(LOGTAG, "Requested synthesis of a pointer-cancel for a pointer that doesn't exist!");
+ return;
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (pointerIndex < 0) {
+ // Adding a new pointer
+ pointerIndex = mPointerState.addPointer(pointerId, source);
+ if (pointerIndex == 0) {
+ // first pointer
+ eventType = MotionEvent.ACTION_DOWN;
+ mPointerState.downTime = SystemClock.uptimeMillis();
+ }
+ } else {
+ // We're moving an existing pointer
+ eventType = MotionEvent.ACTION_MOVE;
+ }
+ break;
+ case MotionEvent.ACTION_HOVER_MOVE:
+ if (pointerIndex < 0) {
+ // Mouse-move a pointer without it going "down". However
+ // in order to send the right MotionEvent without a lot of
+ // duplicated code, we add the pointer to mPointerState,
+ // and then remove it at the bottom of this function.
+ pointerIndex = mPointerState.addPointer(pointerId, source);
+ } else {
+ // We're moving an existing mouse pointer that went down.
+ eventType = MotionEvent.ACTION_MOVE;
+ }
+ break;
+ }
+
+ // Update the pointer with the new info
+ PointerInfo info = mPointerState.pointers.get(pointerIndex);
+ info.screenX = screenX;
+ info.screenY = screenY;
+ info.pressure = pressure;
+ info.orientation = orientation;
+
+ // Dispatch the event
+ int action = (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+ action &= MotionEvent.ACTION_POINTER_INDEX_MASK;
+ action |= (eventType & MotionEvent.ACTION_MASK);
+ boolean isButtonDown = (source == InputDevice.SOURCE_MOUSE) &&
+ (eventType == MotionEvent.ACTION_DOWN || eventType == MotionEvent.ACTION_MOVE);
+ final MotionEvent event = MotionEvent.obtain(
+ /*downTime*/ mPointerState.downTime,
+ /*eventTime*/ SystemClock.uptimeMillis(),
+ /*action*/ action,
+ /*pointerCount*/ mPointerState.getPointerCount(source),
+ /*pointerProperties*/ mPointerState.getPointerProperties(source),
+ /*pointerCoords*/ mPointerState.getPointerCoords(source),
+ /*metaState*/ 0,
+ /*buttonState*/ (isButtonDown ? MotionEvent.BUTTON_PRIMARY : 0),
+ /*xPrecision*/ 0,
+ /*yPrecision*/ 0,
+ /*deviceId*/ 0,
+ /*edgeFlags*/ 0,
+ /*source*/ source,
+ /*flags*/ 0);
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ event.offsetLocation(0, mView.getSurfaceTranslation());
+ mView.dispatchTouchEvent(event);
+ }
+ });
+
+ // Forget about removed pointers
+ if (eventType == MotionEvent.ACTION_POINTER_UP ||
+ eventType == MotionEvent.ACTION_UP ||
+ eventType == MotionEvent.ACTION_CANCEL ||
+ eventType == MotionEvent.ACTION_HOVER_MOVE)
+ {
+ mPointerState.pointers.remove(pointerIndex);
+ }
+ }
+
+ @WrapForJNI
+ public void synthesizeNativeTouchPoint(int pointerId, int eventType, int screenX,
+ int screenY, double pressure, int orientation)
+ {
+ if (pointerId == PointerInfo.RESERVED_MOUSE_POINTER_ID) {
+ throw new IllegalArgumentException("Use a different pointer ID in your test, this one is reserved for mouse");
+ }
+ synthesizeNativePointer(InputDevice.SOURCE_TOUCHSCREEN, pointerId,
+ eventType, screenX, screenY, pressure, orientation);
+ }
+
+ @WrapForJNI
+ public void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
+ synthesizeNativePointer(InputDevice.SOURCE_MOUSE, PointerInfo.RESERVED_MOUSE_POINTER_ID,
+ eventType, screenX, screenY, 0, 0);
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public LayerRenderer.Frame createFrame() {
+ // Create the shaders and textures if necessary.
+ if (!mLayerRendererInitialized) {
+ if (mLayerRenderer == null) {
+ return null;
+ }
+ mLayerRenderer.checkMonitoringEnabled();
+ mLayerRenderer.createDefaultProgram();
+ mLayerRendererInitialized = true;
+ }
+
+ try {
+ return mLayerRenderer.createFrame(mFrameMetrics);
+ } catch (Exception e) {
+ Log.w(LOGTAG, e);
+ return null;
+ }
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public void activateProgram() {
+ mLayerRenderer.activateDefaultProgram();
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public void deactivateProgramAndRestoreState(boolean enableScissor,
+ int scissorX, int scissorY, int scissorW, int scissorH)
+ {
+ mLayerRenderer.deactivateDefaultProgram();
+ mLayerRenderer.restoreState(enableScissor, scissorX, scissorY, scissorW, scissorH);
+ }
+
+ private void geometryChanged(DisplayPortMetrics displayPort) {
+ /* Let Gecko know if the screensize has changed */
+ sendResizeEventIfNecessary(false, null);
+ if (getRedrawHint()) {
+ adjustViewport(displayPort);
+ }
+ }
+
+ /** Implementation of LayerView.Listener */
+ @Override
+ public void renderRequested() {
+ final GLController glController = mView.getGLController();
+ if (glController != null) {
+ glController.invalidateAndScheduleComposite();
+ }
+ }
+
+ /** Implementation of LayerView.Listener */
+ @Override
+ public void sizeChanged(int width, int height) {
+ // We need to make sure a draw happens synchronously at this point,
+ // but resizing the surface before the SurfaceView has resized will
+ // cause a visible jump.
+ mView.getGLController().resumeCompositor(width, height);
+ }
+
+ /** Implementation of LayerView.Listener */
+ @Override
+ public void surfaceChanged(int width, int height) {
+ IntSize viewportSize = mToolbarAnimator.getViewportSize();
+ setViewportSize(viewportSize.width, viewportSize.height, null);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public ImmutableViewportMetrics getViewportMetrics() {
+ return mViewportMetrics;
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public FullScreenState getFullScreenState() {
+ return mView.getFullScreenState();
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public PointF getVisibleEndOfLayerView() {
+ return mToolbarAnimator.getVisibleEndOfLayerView();
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void setAnimationTarget(ImmutableViewportMetrics metrics) {
+ if (mGeckoIsReady) {
+ // We know what the final viewport of the animation is going to be, so
+ // immediately request a draw of that area by setting the display port
+ // accordingly. This way we should have the content pre-rendered by the
+ // time the animation is done.
+ DisplayPortMetrics displayPort = DisplayPortCalculator.calculate(metrics, null);
+ adjustViewport(displayPort);
+ }
+ }
+
+ /** Implementation of PanZoomTarget
+ * You must hold the monitor while calling this.
+ */
+ @Override
+ public void setViewportMetrics(ImmutableViewportMetrics metrics) {
+ setViewportMetrics(metrics, true);
+ }
+
+ /*
+ * You must hold the monitor while calling this.
+ */
+ private void setViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko) {
+ // This class owns the viewport size and the fixed layer margins; don't let other pieces
+ // of code clobber either of them. The only place the viewport size should ever be
+ // updated is in GeckoLayerClient.setViewportSize, and the only place the margins should
+ // ever be updated is in GeckoLayerClient.setFixedLayerMargins; both of these assign to
+ // mViewportMetrics directly.
+ metrics = metrics.setViewportSize(mViewportMetrics.viewportRectWidth, mViewportMetrics.viewportRectHeight);
+ mViewportMetrics = metrics;
+
+ viewportMetricsChanged(notifyGecko);
+ }
+
+ /*
+ * You must hold the monitor while calling this.
+ */
+ private void viewportMetricsChanged(boolean notifyGecko) {
+ mToolbarAnimator.onMetricsChanged(mViewportMetrics);
+
+ mView.requestRender();
+ if (notifyGecko && mGeckoIsReady) {
+ geometryChanged(null);
+ }
+ }
+
+ /*
+ * Updates the viewport metrics, overriding the viewport size and margins
+ * which are normally retained when calling setViewportMetrics.
+ * You must hold the monitor while calling this.
+ */
+ void forceViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko, boolean forceRedraw) {
+ if (forceRedraw) {
+ mForceRedraw = true;
+ }
+ mViewportMetrics = metrics;
+ viewportMetricsChanged(notifyGecko);
+ }
+
+ /** Implementation of PanZoomTarget
+ * Scroll the viewport by a certain amount. This will take viewport margins
+ * and margin animation into account. If margins are currently animating,
+ * this will just go ahead and modify the viewport origin, otherwise the
+ * delta will be applied to the margins and the remainder will be applied to
+ * the viewport origin.
+ *
+ * You must hold the monitor while calling this.
+ */
+ @Override
+ public void scrollBy(float dx, float dy) {
+ // Set mViewportMetrics manually so the margin changes take.
+ mViewportMetrics = mViewportMetrics.offsetViewportBy(dx, dy);
+ viewportMetricsChanged(true);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void panZoomStopped() {
+ mToolbarAnimator.onPanZoomStopped();
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void forceRedraw(DisplayPortMetrics displayPort) {
+ mForceRedraw = true;
+ if (mGeckoIsReady) {
+ geometryChanged(displayPort);
+ }
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public boolean post(Runnable action) {
+ return mView.post(action);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void postRenderTask(RenderTask task) {
+ mView.postRenderTask(task);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void removeRenderTask(RenderTask task) {
+ mView.removeRenderTask(task);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public Object getLock() {
+ return this;
+ }
+
+ /** Implementation of PanZoomTarget
+ * Converts a point from layer view coordinates to layer coordinates. In other words, given a
+ * point measured in pixels from the top left corner of the layer view, returns the point in
+ * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the
+ * events being sent to Gecko are processed in FIFO order, this calculation should always be
+ * correct.
+ */
+ @Override
+ public PointF convertViewPointToLayerPoint(PointF viewPoint) {
+ if (!mGeckoIsReady) {
+ return null;
+ }
+
+ ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+ PointF origin = viewportMetrics.getOrigin();
+ float zoom = viewportMetrics.zoomFactor;
+ ImmutableViewportMetrics geckoViewport = (AppConstants.MOZ_ANDROID_APZ ? mViewportMetrics : mGeckoViewport);
+ PointF geckoOrigin = geckoViewport.getOrigin();
+ float geckoZoom = geckoViewport.zoomFactor;
+
+ // viewPoint + origin - offset gives the coordinate in device pixels from the top-left corner of the page.
+ // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page.
+ // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from
+ // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from
+ // the current Gecko coordinate in CSS pixels.
+ PointF layerPoint = new PointF(
+ ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
+ ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
+
+ return layerPoint;
+ }
+
+ @Override
+ public void setScrollingRootContent(boolean isRootContent) {
+ mToolbarAnimator.setScrollingRootContent(isRootContent);
+ }
+
+ public void addDrawListener(DrawListener listener) {
+ mDrawListeners.add(listener);
+ }
+
+ public void removeDrawListener(DrawListener listener) {
+ mDrawListeners.remove(listener);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
@@ -0,0 +1,316 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+
+/**
+ * ImmutableViewportMetrics are used to store the viewport metrics
+ * in way that we can access a version of them from multiple threads
+ * without having to take a lock
+ */
+public class ImmutableViewportMetrics {
+
+ // We need to flatten the RectF and FloatSize structures
+ // because Java doesn't have the concept of const classes
+ public final float pageRectLeft;
+ public final float pageRectTop;
+ public final float pageRectRight;
+ public final float pageRectBottom;
+ public final float cssPageRectLeft;
+ public final float cssPageRectTop;
+ public final float cssPageRectRight;
+ public final float cssPageRectBottom;
+ public final float viewportRectLeft;
+ public final float viewportRectTop;
+ public final int viewportRectWidth;
+ public final int viewportRectHeight;
+
+ public final float zoomFactor;
+ public final boolean isRTL;
+
+ public ImmutableViewportMetrics(DisplayMetrics metrics) {
+ viewportRectLeft = pageRectLeft = cssPageRectLeft = 0;
+ viewportRectTop = pageRectTop = cssPageRectTop = 0;
+ viewportRectWidth = metrics.widthPixels;
+ viewportRectHeight = metrics.heightPixels;
+ pageRectRight = cssPageRectRight = metrics.widthPixels;
+ pageRectBottom = cssPageRectBottom = metrics.heightPixels;
+ zoomFactor = 1.0f;
+ isRTL = false;
+ }
+
+ /** This constructor is used by native code in AndroidJavaWrappers.cpp, be
+ * careful when modifying the signature.
+ */
+ @WrapForJNI(allowMultithread = true)
+ public ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
+ float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
+ float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
+ float aViewportRectLeft, float aViewportRectTop, int aViewportRectWidth,
+ int aViewportRectHeight, float aZoomFactor)
+ {
+ this(aPageRectLeft, aPageRectTop,
+ aPageRectRight, aPageRectBottom, aCssPageRectLeft,
+ aCssPageRectTop, aCssPageRectRight, aCssPageRectBottom,
+ aViewportRectLeft, aViewportRectTop, aViewportRectWidth,
+ aViewportRectHeight, aZoomFactor, false);
+ }
+
+ private ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
+ float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
+ float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
+ float aViewportRectLeft, float aViewportRectTop, int aViewportRectWidth,
+ int aViewportRectHeight, float aZoomFactor, boolean aIsRTL)
+ {
+ pageRectLeft = aPageRectLeft;
+ pageRectTop = aPageRectTop;
+ pageRectRight = aPageRectRight;
+ pageRectBottom = aPageRectBottom;
+ cssPageRectLeft = aCssPageRectLeft;
+ cssPageRectTop = aCssPageRectTop;
+ cssPageRectRight = aCssPageRectRight;
+ cssPageRectBottom = aCssPageRectBottom;
+ viewportRectLeft = aViewportRectLeft;
+ viewportRectTop = aViewportRectTop;
+ viewportRectWidth = aViewportRectWidth;
+ viewportRectHeight = aViewportRectHeight;
+ zoomFactor = aZoomFactor;
+ isRTL = aIsRTL;
+ }
+
+ public float getWidth() {
+ return viewportRectWidth;
+ }
+
+ public float getHeight() {
+ return viewportRectHeight;
+ }
+
+ public float viewportRectRight() {
+ return viewportRectLeft + viewportRectWidth;
+ }
+
+ public float viewportRectBottom() {
+ return viewportRectTop + viewportRectHeight;
+ }
+
+ public PointF getOrigin() {
+ return new PointF(viewportRectLeft, viewportRectTop);
+ }
+
+ public FloatSize getSize() {
+ return new FloatSize(viewportRectWidth, viewportRectHeight);
+ }
+
+ public RectF getViewport() {
+ return new RectF(viewportRectLeft,
+ viewportRectTop,
+ viewportRectRight(),
+ viewportRectBottom());
+ }
+
+ public RectF getCssViewport() {
+ return RectUtils.scale(getViewport(), 1 / zoomFactor);
+ }
+
+ public RectF getPageRect() {
+ return new RectF(pageRectLeft, pageRectTop, pageRectRight, pageRectBottom);
+ }
+
+ public float getPageWidth() {
+ return pageRectRight - pageRectLeft;
+ }
+
+ public float getPageHeight() {
+ return pageRectBottom - pageRectTop;
+ }
+
+ public RectF getCssPageRect() {
+ return new RectF(cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom);
+ }
+
+ public RectF getOverscroll() {
+ return new RectF(Math.max(0, pageRectLeft - viewportRectLeft),
+ Math.max(0, pageRectTop - viewportRectTop),
+ Math.max(0, viewportRectRight() - pageRectRight),
+ Math.max(0, viewportRectBottom() - pageRectBottom));
+ }
+
+ /*
+ * Returns the viewport metrics that represent a linear transition between "this" and "to" at
+ * time "t", which is on the scale [0, 1). This function interpolates all values stored in
+ * the viewport metrics.
+ */
+ public ImmutableViewportMetrics interpolate(ImmutableViewportMetrics to, float t) {
+ return new ImmutableViewportMetrics(
+ FloatUtils.interpolate(pageRectLeft, to.pageRectLeft, t),
+ FloatUtils.interpolate(pageRectTop, to.pageRectTop, t),
+ FloatUtils.interpolate(pageRectRight, to.pageRectRight, t),
+ FloatUtils.interpolate(pageRectBottom, to.pageRectBottom, t),
+ FloatUtils.interpolate(cssPageRectLeft, to.cssPageRectLeft, t),
+ FloatUtils.interpolate(cssPageRectTop, to.cssPageRectTop, t),
+ FloatUtils.interpolate(cssPageRectRight, to.cssPageRectRight, t),
+ FloatUtils.interpolate(cssPageRectBottom, to.cssPageRectBottom, t),
+ FloatUtils.interpolate(viewportRectLeft, to.viewportRectLeft, t),
+ FloatUtils.interpolate(viewportRectTop, to.viewportRectTop, t),
+ (int)FloatUtils.interpolate(viewportRectWidth, to.viewportRectWidth, t),
+ (int)FloatUtils.interpolate(viewportRectHeight, to.viewportRectHeight, t),
+ FloatUtils.interpolate(zoomFactor, to.zoomFactor, t),
+ t >= 0.5 ? to.isRTL : isRTL);
+ }
+
+ public ImmutableViewportMetrics setViewportSize(int width, int height) {
+ if (width == viewportRectWidth && height == viewportRectHeight) {
+ return this;
+ }
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, width, height,
+ zoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newOriginX, newOriginY, viewportRectWidth, viewportRectHeight,
+ zoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
+ newZoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
+ return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
+ }
+
+ public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
+ if (isRTL) {
+ return setViewportOrigin(
+ Math.min(pageRectRight - getWidth(), Math.max(viewportRectLeft + dx, pageRectLeft)),
+ Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
+ }
+ return setViewportOrigin(
+ Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidth())),
+ Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
+ }
+
+ public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) {
+ return new ImmutableViewportMetrics(
+ pageRect.left, pageRect.top, pageRect.right, pageRect.bottom,
+ cssPageRect.left, cssPageRect.top, cssPageRect.right, cssPageRect.bottom,
+ viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
+ zoomFactor, isRTL);
+ }
+
+ public ImmutableViewportMetrics setPageRectFrom(ImmutableViewportMetrics aMetrics) {
+ if (aMetrics.cssPageRectLeft == cssPageRectLeft &&
+ aMetrics.cssPageRectTop == cssPageRectTop &&
+ aMetrics.cssPageRectRight == cssPageRectRight &&
+ aMetrics.cssPageRectBottom == cssPageRectBottom) {
+ return this;
+ }
+ RectF css = aMetrics.getCssPageRect();
+ return setPageRect(RectUtils.scale(css, zoomFactor), css);
+ }
+
+ public ImmutableViewportMetrics setIsRTL(boolean aIsRTL) {
+ if (isRTL == aIsRTL) {
+ return this;
+ }
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
+ zoomFactor, aIsRTL);
+ }
+
+ /* This will set the zoom factor and re-scale page-size and viewport offset
+ * accordingly. The given focus will remain at the same point on the screen
+ * after scaling.
+ */
+ public ImmutableViewportMetrics scaleTo(float newZoomFactor, PointF focus) {
+ // cssPageRect* is invariant, since we're setting the scale factor
+ // here. The page rect is based on the CSS page rect.
+ float newPageRectLeft = cssPageRectLeft * newZoomFactor;
+ float newPageRectTop = cssPageRectTop * newZoomFactor;
+ float newPageRectRight = cssPageRectLeft + ((cssPageRectRight - cssPageRectLeft) * newZoomFactor);
+ float newPageRectBottom = cssPageRectTop + ((cssPageRectBottom - cssPageRectTop) * newZoomFactor);
+
+ PointF origin = getOrigin();
+ origin.offset(focus.x, focus.y);
+ origin = PointUtils.scale(origin, newZoomFactor / zoomFactor);
+ origin.offset(-focus.x, -focus.y);
+
+ return new ImmutableViewportMetrics(
+ newPageRectLeft, newPageRectTop, newPageRectRight, newPageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ origin.x, origin.y, viewportRectWidth, viewportRectHeight,
+ newZoomFactor, isRTL);
+ }
+
+ /** Clamps the viewport to remain within the page rect. */
+ public ImmutableViewportMetrics clamp() {
+ RectF newViewport = getViewport();
+
+ // The viewport bounds ought to never exceed the page bounds.
+ if (newViewport.right > pageRectRight)
+ newViewport.offset((pageRectRight) - newViewport.right, 0);
+ if (newViewport.left < pageRectLeft)
+ newViewport.offset(pageRectLeft - newViewport.left, 0);
+
+ if (newViewport.bottom > pageRectBottom)
+ newViewport.offset(0, (pageRectBottom) - newViewport.bottom);
+ if (newViewport.top < pageRectTop)
+ newViewport.offset(0, pageRectTop - newViewport.top);
+
+ // Note that since newViewport is only translated around, the viewport's
+ // width and height are unchanged.
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newViewport.left, newViewport.top, viewportRectWidth, viewportRectHeight,
+ zoomFactor, isRTL);
+ }
+
+ public boolean fuzzyEquals(ImmutableViewportMetrics other) {
+ // Don't bother checking the pageRectXXX values because they are a product
+ // of the cssPageRectXXX values and the zoomFactor, except with more rounding
+ // error. Checking those is both inefficient and can lead to false negatives.
+ return FloatUtils.fuzzyEquals(cssPageRectLeft, other.cssPageRectLeft)
+ && FloatUtils.fuzzyEquals(cssPageRectTop, other.cssPageRectTop)
+ && FloatUtils.fuzzyEquals(cssPageRectRight, other.cssPageRectRight)
+ && FloatUtils.fuzzyEquals(cssPageRectBottom, other.cssPageRectBottom)
+ && FloatUtils.fuzzyEquals(viewportRectLeft, other.viewportRectLeft)
+ && FloatUtils.fuzzyEquals(viewportRectTop, other.viewportRectTop)
+ && viewportRectWidth == other.viewportRectWidth
+ && viewportRectHeight == other.viewportRectHeight
+ && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
+ }
+
+ @Override
+ public String toString() {
+ return "ImmutableViewportMetrics v=(" + viewportRectLeft + "," + viewportRectTop + ","
+ + viewportRectWidth + "x" + viewportRectHeight + ") p=(" + pageRectLeft + ","
+ + pageRectTop + "," + pageRectRight + "," + pageRectBottom + ") c=("
+ + cssPageRectLeft + "," + cssPageRectTop + "," + cssPageRectRight + ","
+ + cssPageRectBottom + ") z=" + zoomFactor + ", rtl=" + isRTL;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java
@@ -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.gfx;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class IntSize {
+ public final int width, height;
+
+ public IntSize(IntSize size) { width = size.width; height = size.height; }
+ public IntSize(int inWidth, int inHeight) { width = inWidth; height = inHeight; }
+
+ public IntSize(FloatSize size) {
+ width = Math.round(size.width);
+ height = Math.round(size.height);
+ }
+
+ public IntSize(JSONObject json) {
+ try {
+ width = json.getInt("width");
+ height = json.getInt("height");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getArea() {
+ return width * height;
+ }
+
+ public boolean equals(IntSize size) {
+ return ((size.width == width) && (size.height == height));
+ }
+
+ public boolean isPositive() {
+ return (width > 0 && height > 0);
+ }
+
+ @Override
+ public String toString() { return "(" + width + "," + height + ")"; }
+
+ public IntSize scale(float factor) {
+ return new IntSize(Math.round(width * factor),
+ Math.round(height * factor));
+ }
+
+ /* Returns the power of two that is greater than or equal to value */
+ public static int nextPowerOfTwo(int value) {
+ // code taken from http://acius2.blogspot.com/2007/11/calculating-next-power-of-2.html
+ if (0 == value--) {
+ return 1;
+ }
+ value = (value >> 1) | value;
+ value = (value >> 2) | value;
+ value = (value >> 4) | value;
+ value = (value >> 8) | value;
+ value = (value >> 16) | value;
+ return value + 1;
+ }
+
+ public IntSize nextPowerOfTwo() {
+ return new IntSize(nextPowerOfTwo(width), nextPowerOfTwo(height));
+ }
+
+ public static boolean isPowerOfTwo(int value) {
+ if (value == 0)
+ return false;
+ return (value & (value - 1)) == 0;
+ }
+
+ public static int largestPowerOfTwoLessThan(float value) {
+ int val = (int) Math.floor(value);
+ if (val <= 0) {
+ throw new IllegalArgumentException("Error: value must be > 0");
+ }
+ // keep dropping the least-significant set bits until only one is left
+ int bestVal = val;
+ while (val != 0) {
+ bestVal = val;
+ val &= (val - 1);
+ }
+ return bestVal;
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Layer.java
@@ -0,0 +1,204 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import java.nio.FloatBuffer;
+import java.util.concurrent.locks.ReentrantLock;
+
+public abstract class Layer {
+ private final ReentrantLock mTransactionLock;
+ private boolean mInTransaction;
+ private Rect mNewPosition;
+ private float mNewResolution;
+
+ protected Rect mPosition;
+ protected float mResolution;
+
+ public Layer() {
+ this(null);
+ }
+
+ public Layer(IntSize size) {
+ mTransactionLock = new ReentrantLock();
+ if (size == null) {
+ mPosition = new Rect();
+ } else {
+ mPosition = new Rect(0, 0, size.width, size.height);
+ }
+ mResolution = 1.0f;
+ }
+
+ /**
+ * Updates the layer. This returns false if there is still work to be done
+ * after this update.
+ */
+ public final boolean update(RenderContext context) {
+ if (mTransactionLock.isHeldByCurrentThread()) {
+ throw new RuntimeException("draw() called while transaction lock held by this " +
+ "thread?!");
+ }
+
+ if (mTransactionLock.tryLock()) {
+ try {
+ performUpdates(context);
+ return true;
+ } finally {
+ mTransactionLock.unlock();
+ }
+ }
+
+ return false;
+ }
+
+ /** Subclasses override this function to draw the layer. */
+ public abstract void draw(RenderContext context);
+
+ /** Given the intrinsic size of the layer, returns the pixel boundaries of the layer rect. */
+ protected RectF getBounds(RenderContext context) {
+ return RectUtils.scale(new RectF(mPosition), context.zoomFactor / mResolution);
+ }
+
+ /**
+ * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer"
+ * includes altering the underlying BufferedImage in any way. Thus you must call this function
+ * before modifying the byte buffer associated with this layer.
+ *
+ * This function may block, so you should never call this on the main UI thread.
+ */
+ public void beginTransaction() {
+ if (mTransactionLock.isHeldByCurrentThread())
+ throw new RuntimeException("Nested transactions are not supported");
+ mTransactionLock.lock();
+ mInTransaction = true;
+ mNewResolution = mResolution;
+ }
+
+ /** Call this when you're done modifying the layer. */
+ public void endTransaction() {
+ if (!mInTransaction)
+ throw new RuntimeException("endTransaction() called outside a transaction");
+ mInTransaction = false;
+ mTransactionLock.unlock();
+ }
+
+ /** Returns true if the layer is currently in a transaction and false otherwise. */
+ protected boolean inTransaction() {
+ return mInTransaction;
+ }
+
+ /** Returns the current layer position. */
+ public Rect getPosition() {
+ return mPosition;
+ }
+
+ /** Sets the position. Only valid inside a transaction. */
+ public void setPosition(Rect newPosition) {
+ if (!mInTransaction)
+ throw new RuntimeException("setPosition() is only valid inside a transaction");
+ mNewPosition = newPosition;
+ }
+
+ /** Returns the current layer's resolution. */
+ public float getResolution() {
+ return mResolution;
+ }
+
+ /**
+ * Sets the layer resolution. This value is used to determine how many pixels per
+ * device pixel this layer was rendered at. This will be reflected by scaling by
+ * the reciprocal of the resolution in the layer's transform() function.
+ * Only valid inside a transaction. */
+ public void setResolution(float newResolution) {
+ if (!mInTransaction)
+ throw new RuntimeException("setResolution() is only valid inside a transaction");
+ mNewResolution = newResolution;
+ }
+
+ /**
+ * Subclasses may override this method to perform custom layer updates. This will be called
+ * with the transaction lock held. Subclass implementations of this method must call the
+ * superclass implementation. Returns false if there is still work to be done after this
+ * update is complete.
+ */
+ protected void performUpdates(RenderContext context) {
+ if (mNewPosition != null) {
+ mPosition = mNewPosition;
+ mNewPosition = null;
+ }
+ if (mNewResolution != 0.0f) {
+ mResolution = mNewResolution;
+ mNewResolution = 0.0f;
+ }
+ }
+
+ /**
+ * This function fills in the provided <tt>dest</tt> array with values to render a texture.
+ * The array is filled with 4 sets of {x, y, z, texture_x, texture_y} values (so 20 values
+ * in total) corresponding to the corners of the rect.
+ */
+ protected final void fillRectCoordBuffer(float[] dest, RectF rect, float viewWidth, float viewHeight,
+ Rect cropRect, float texWidth, float texHeight) {
+ //x, y, z, texture_x, texture_y
+ dest[0] = rect.left / viewWidth;
+ dest[1] = rect.bottom / viewHeight;
+ dest[2] = 0;
+ dest[3] = cropRect.left / texWidth;
+ dest[4] = cropRect.top / texHeight;
+
+ dest[5] = rect.left / viewWidth;
+ dest[6] = rect.top / viewHeight;
+ dest[7] = 0;
+ dest[8] = cropRect.left / texWidth;
+ dest[9] = cropRect.bottom / texHeight;
+
+ dest[10] = rect.right / viewWidth;
+ dest[11] = rect.bottom / viewHeight;
+ dest[12] = 0;
+ dest[13] = cropRect.right / texWidth;
+ dest[14] = cropRect.top / texHeight;
+
+ dest[15] = rect.right / viewWidth;
+ dest[16] = rect.top / viewHeight;
+ dest[17] = 0;
+ dest[18] = cropRect.right / texWidth;
+ dest[19] = cropRect.bottom / texHeight;
+ }
+
+ public static class RenderContext {
+ public final RectF viewport;
+ public final RectF pageRect;
+ public final float zoomFactor;
+ public final int positionHandle;
+ public final int textureHandle;
+ public final FloatBuffer coordBuffer;
+
+ public RenderContext(RectF aViewport, RectF aPageRect, float aZoomFactor,
+ int aPositionHandle, int aTextureHandle, FloatBuffer aCoordBuffer) {
+ viewport = aViewport;
+ pageRect = aPageRect;
+ zoomFactor = aZoomFactor;
+ positionHandle = aPositionHandle;
+ textureHandle = aTextureHandle;
+ coordBuffer = aCoordBuffer;
+ }
+
+ public boolean fuzzyEquals(RenderContext other) {
+ if (other == null) {
+ return false;
+ }
+ return RectUtils.fuzzyEquals(viewport, other.viewport)
+ && RectUtils.fuzzyEquals(pageRect, other.pageRect)
+ && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
+ }
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
@@ -0,0 +1,510 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.Layer.RenderContext;
+import org.mozilla.gecko.mozglue.DirectBufferAllocator;
+
+import android.graphics.Color;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.microedition.khronos.egl.EGLConfig;
+
+/**
+ * The layer renderer implements the rendering logic for a layer view.
+ */
+public class LayerRenderer {
+ private static final String LOGTAG = "GeckoLayerRenderer";
+ private static final String PROFTAG = "GeckoLayerRendererProf";
+
+ private static final int NANOS_PER_SECOND = 1000000000;
+
+ private static final int MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER = 5;
+
+ private final LayerView mView;
+ private ByteBuffer mCoordByteBuffer;
+ private FloatBuffer mCoordBuffer;
+ private int mMaxTextureSize;
+ private int mBackgroundColor;
+
+ private long mLastFrameTime;
+ private final CopyOnWriteArrayList<RenderTask> mTasks;
+
+ private final CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>();
+
+ // Render profiling output
+ private int mFramesRendered;
+ private float mCompleteFramesRendered;
+ private boolean mProfileRender;
+ private long mProfileOutputTime;
+
+ private IntBuffer mPixelBuffer;
+
+ // Used by GLES 2.0
+ private int mProgram;
+ private int mPositionHandle;
+ private int mTextureHandle;
+ private int mSampleHandle;
+ private int mTMatrixHandle;
+
+ private List<LayerView.ZoomedViewListener> mZoomedViewListeners;
+ private float mLastViewLeft;
+ private float mLastViewTop;
+
+ // column-major matrix applied to each vertex to shift the viewport from
+ // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by
+ // a factor of 2 to fill up the screen
+ public static final float[] DEFAULT_TEXTURE_MATRIX = {
+ 2.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 2.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 2.0f, 0.0f,
+ -1.0f, -1.0f, 0.0f, 1.0f
+ };
+
+ private static final int COORD_BUFFER_SIZE = 20;
+
+ // The shaders run on the GPU directly, the vertex shader is only applying the
+ // matrix transform detailed above
+
+ // Note we flip the y-coordinate in the vertex shader from a
+ // coordinate system with (0,0) in the top left to one with (0,0) in
+ // the bottom left.
+
+ public static final String DEFAULT_VERTEX_SHADER =
+ "uniform mat4 uTMatrix;\n" +
+ "attribute vec4 vPosition;\n" +
+ "attribute vec2 aTexCoord;\n" +
+ "varying vec2 vTexCoord;\n" +
+ "void main() {\n" +
+ " gl_Position = uTMatrix * vPosition;\n" +
+ " vTexCoord.x = aTexCoord.x;\n" +
+ " vTexCoord.y = 1.0 - aTexCoord.y;\n" +
+ "}\n";
+
+ // We use highp because the screenshot textures
+ // we use are large and we stretch them alot
+ // so we need all the precision we can get.
+ // Unfortunately, highp is not required by ES 2.0
+ // so on GPU's like Mali we end up getting mediump
+ public static final String DEFAULT_FRAGMENT_SHADER =
+ "precision highp float;\n" +
+ "varying vec2 vTexCoord;\n" +
+ "uniform sampler2D sTexture;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(sTexture, vTexCoord);\n" +
+ "}\n";
+
+ public LayerRenderer(LayerView view) {
+ mView = view;
+
+ mTasks = new CopyOnWriteArrayList<RenderTask>();
+ mLastFrameTime = System.nanoTime();
+
+ mZoomedViewListeners = new ArrayList<LayerView.ZoomedViewListener>();
+ }
+
+ public void destroy() {
+ if (mCoordByteBuffer != null) {
+ DirectBufferAllocator.free(mCoordByteBuffer);
+ mCoordByteBuffer = null;
+ mCoordBuffer = null;
+ }
+ mZoomedViewListeners.clear();
+ }
+
+ void onSurfaceCreated(EGLConfig config) {
+ checkMonitoringEnabled();
+ createDefaultProgram();
+ activateDefaultProgram();
+ }
+
+ public void createDefaultProgram() {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
+ int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER);
+
+ mProgram = GLES20.glCreateProgram();
+ GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
+ GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
+ GLES20.glLinkProgram(mProgram); // creates OpenGL program executables
+
+ // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members.
+ mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+ mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
+ mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
+ mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix");
+
+ int maxTextureSizeResult[] = new int[1];
+ GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
+ mMaxTextureSize = maxTextureSizeResult[0];
+ }
+
+ // Activates the shader program.
+ public void activateDefaultProgram() {
+ // Add the program to the OpenGL environment
+ GLES20.glUseProgram(mProgram);
+
+ // Set the transformation matrix
+ GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, DEFAULT_TEXTURE_MATRIX, 0);
+
+ // Enable the arrays from which we get the vertex and texture coordinates
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+ GLES20.glEnableVertexAttribArray(mTextureHandle);
+
+ GLES20.glUniform1i(mSampleHandle, 0);
+
+ // TODO: Move these calls into a separate deactivate() call that is called after the
+ // underlay and overlay are rendered.
+ }
+
+ // Deactivates the shader program. This must be done to avoid crashes after returning to the
+ // Gecko C++ compositor from Java.
+ public void deactivateDefaultProgram() {
+ GLES20.glDisableVertexAttribArray(mTextureHandle);
+ GLES20.glDisableVertexAttribArray(mPositionHandle);
+ GLES20.glUseProgram(0);
+ }
+
+ void restoreState(boolean enableScissor, int scissorX, int scissorY, int scissorW, int scissorH) {
+ GLES20.glScissor(scissorX, scissorY, scissorW, scissorH);
+ if (enableScissor) {
+ GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+ } else {
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+ }
+ }
+
+ public int getMaxTextureSize() {
+ return mMaxTextureSize;
+ }
+
+ public void postRenderTask(RenderTask aTask) {
+ mTasks.add(aTask);
+ mView.requestRender();
+ }
+
+ public void removeRenderTask(RenderTask aTask) {
+ mTasks.remove(aTask);
+ }
+
+ private void runRenderTasks(CopyOnWriteArrayList<RenderTask> tasks, boolean after, long frameStartTime) {
+ for (RenderTask task : tasks) {
+ if (task.runAfter != after) {
+ continue;
+ }
+
+ boolean stillRunning = task.run(frameStartTime - mLastFrameTime, frameStartTime);
+
+ // Remove the task from the list if its finished
+ if (!stillRunning) {
+ tasks.remove(task);
+ }
+ }
+ }
+
+ public void addLayer(Layer layer) {
+ synchronized (mExtraLayers) {
+ if (mExtraLayers.contains(layer)) {
+ mExtraLayers.remove(layer);
+ }
+
+ mExtraLayers.add(layer);
+ }
+ }
+
+ public void removeLayer(Layer layer) {
+ synchronized (mExtraLayers) {
+ mExtraLayers.remove(layer);
+ }
+ }
+
+ private void printCheckerboardStats() {
+ Log.d(PROFTAG, "Frames rendered over last 1000ms: " + mCompleteFramesRendered + "/" + mFramesRendered);
+ mFramesRendered = 0;
+ mCompleteFramesRendered = 0;
+ }
+
+ /** Used by robocop for testing purposes. Not for production use! */
+ IntBuffer getPixels() {
+ IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight());
+ synchronized (pixelBuffer) {
+ mPixelBuffer = pixelBuffer;
+ mView.requestRender();
+ try {
+ pixelBuffer.wait();
+ } catch (InterruptedException ie) {
+ }
+ mPixelBuffer = null;
+ }
+ return pixelBuffer;
+ }
+
+ private RenderContext createScreenContext(ImmutableViewportMetrics metrics) {
+ RectF viewport = new RectF(0.0f, 0.0f, metrics.getWidth(), metrics.getHeight());
+ RectF pageRect = metrics.getPageRect();
+
+ return createContext(viewport, pageRect, 1.0f);
+ }
+
+ private RenderContext createPageContext(ImmutableViewportMetrics metrics) {
+ RectF viewport = metrics.getViewport();
+ RectF pageRect = metrics.getPageRect();
+ float zoomFactor = metrics.zoomFactor;
+
+ return createContext(new RectF(RectUtils.round(viewport)), pageRect, zoomFactor);
+ }
+
+ private RenderContext createContext(RectF viewport, RectF pageRect, float zoomFactor) {
+ if (mCoordBuffer == null) {
+ // Initialize the FloatBuffer that will be used to store all vertices and texture
+ // coordinates in draw() commands.
+ mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4);
+ mCoordByteBuffer.order(ByteOrder.nativeOrder());
+ mCoordBuffer = mCoordByteBuffer.asFloatBuffer();
+ if (mCoordBuffer == null) {
+ throw new IllegalStateException();
+ }
+ }
+ return new RenderContext(viewport, pageRect, zoomFactor,
+ mPositionHandle, mTextureHandle, mCoordBuffer);
+ }
+
+ void checkMonitoringEnabled() {
+ mProfileRender = Log.isLoggable(PROFTAG, Log.DEBUG);
+ }
+
+ /*
+ * create a vertex shader type (GLES20.GL_VERTEX_SHADER)
+ * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
+ */
+ public static int loadShader(int type, String shaderCode) {
+ int shader = GLES20.glCreateShader(type);
+ GLES20.glShaderSource(shader, shaderCode);
+ GLES20.glCompileShader(shader);
+ return shader;
+ }
+
+ public Frame createFrame(ImmutableViewportMetrics metrics) {
+ return new Frame(metrics);
+ }
+
+ public class Frame {
+ // The timestamp recording the start of this frame.
+ private long mFrameStartTime;
+ // A fixed snapshot of the viewport metrics that this frame is using to render content.
+ private final ImmutableViewportMetrics mFrameMetrics;
+ // A rendering context for page-positioned layers, and one for screen-positioned layers.
+ private final RenderContext mPageContext, mScreenContext;
+ // Whether a layer was updated.
+ private boolean mUpdated;
+
+ public Frame(ImmutableViewportMetrics metrics) {
+ mFrameMetrics = metrics;
+
+ // Work out the offset due to margins
+ mPageContext = createPageContext(metrics);
+ mScreenContext = createScreenContext(metrics);
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ @WrapForJNI(allowMultithread = true)
+ public void beginDrawing() {
+ mFrameStartTime = System.nanoTime();
+
+ TextureReaper.get().reap();
+ TextureGenerator.get().fill();
+
+ mUpdated = true;
+
+ Layer rootLayer = mView.getLayerClient().getRoot();
+
+ // Run through pre-render tasks
+ runRenderTasks(mTasks, false, mFrameStartTime);
+
+ /* Update layers. */
+ if (rootLayer != null) {
+ // Called on compositor thread.
+ mUpdated &= rootLayer.update(mPageContext);
+ }
+
+ for (Layer layer : mExtraLayers) {
+ mUpdated &= layer.update(mPageContext); // called on compositor thread
+ }
+ }
+
+ private void clear(int color) {
+ GLES20.glClearColor(((color >> 16) & 0xFF) / 255.0f,
+ ((color >> 8) & 0xFF) / 255.0f,
+ (color & 0xFF) / 255.0f,
+ 0.0f);
+ // The bits set here need to match up with those used
+ // in gfx/layers/opengl/LayerManagerOGL.cpp.
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
+ GLES20.GL_DEPTH_BUFFER_BIT);
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ @WrapForJNI(allowMultithread = true)
+ public void drawBackground() {
+ // Any GL state which is changed here must be restored in
+ // restoreState(...)
+
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+
+ // Update background color.
+ mBackgroundColor = mView.getBackgroundColor();
+
+ // Clear the page area to the page background colour.
+ clear(mBackgroundColor);
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ public void drawForeground() {
+ // Any GL state which is changed here must be restored in
+ // restoreState(...)
+
+ /* Draw any extra layers that were added (likely plugins) */
+ if (mExtraLayers.size() > 0) {
+ for (Layer layer : mExtraLayers) {
+ layer.draw(mPageContext);
+ }
+ }
+
+ /* Measure how much of the screen is checkerboarding */
+ Layer rootLayer = mView.getLayerClient().getRoot();
+ if ((rootLayer != null) &&
+ (mProfileRender || PanningPerfAPI.isRecordingCheckerboard())) {
+ // Calculate the incompletely rendered area of the page
+ float checkerboard = 1.0f - GeckoAppShell.computeRenderIntegrity();
+
+ PanningPerfAPI.recordCheckerboard(checkerboard);
+ if (checkerboard < 0.0f || checkerboard > 1.0f) {
+ Log.e(LOGTAG, "Checkerboard value out of bounds: " + checkerboard);
+ }
+
+ mCompleteFramesRendered += 1.0f - checkerboard;
+ mFramesRendered ++;
+
+ if (mFrameStartTime - mProfileOutputTime > NANOS_PER_SECOND) {
+ mProfileOutputTime = mFrameStartTime;
+ printCheckerboardStats();
+ }
+ }
+
+ runRenderTasks(mTasks, true, mFrameStartTime);
+
+ }
+
+ private void maybeRequestZoomedViewRender(RenderContext context) {
+ // Concurrently update of mZoomedViewListeners should not be an issue here
+ // because the following line is just a short-circuit
+ if (mZoomedViewListeners.size() == 0) {
+ return;
+ }
+
+ // When scrolling fast, do not request zoomed view render to avoid to slow down
+ // the scroll in the main view.
+ // Speed is estimated using the offset changes between 2 display frame calls
+ final float viewLeft = context.viewport.left;
+ final float viewTop = context.viewport.top;
+ boolean shouldWaitToRender = false;
+
+ if (Math.abs(mLastViewLeft - viewLeft) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER ||
+ Math.abs(mLastViewTop - viewTop) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER) {
+ shouldWaitToRender = true;
+ }
+
+ mLastViewLeft = viewLeft;
+ mLastViewTop = viewTop;
+
+ if (shouldWaitToRender) {
+ return;
+ }
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (LayerView.ZoomedViewListener listener : mZoomedViewListeners) {
+ listener.requestZoomedViewRender();
+ }
+ }
+ });
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ @WrapForJNI(allowMultithread = true)
+ public void endDrawing() {
+ // If a layer update requires further work, schedule another redraw
+ if (!mUpdated)
+ mView.requestRender();
+
+ PanningPerfAPI.recordFrameTime();
+
+ maybeRequestZoomedViewRender(mPageContext);
+
+ /* Used by robocop for testing purposes */
+ IntBuffer pixelBuffer = mPixelBuffer;
+ if (mUpdated && pixelBuffer != null) {
+ synchronized (pixelBuffer) {
+ pixelBuffer.position(0);
+ GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(),
+ (int)mScreenContext.viewport.height(), GLES20.GL_RGBA,
+ GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
+ pixelBuffer.notify();
+ }
+ }
+
+ // Remove background color once we've painted. GeckoLayerClient is
+ // responsible for setting this flag before current document is
+ // composited.
+ if (mView.getPaintState() == LayerView.PAINT_BEFORE_FIRST) {
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.setSurfaceBackgroundColor(Color.TRANSPARENT);
+ }
+ });
+ mView.setPaintState(LayerView.PAINT_AFTER_FIRST);
+ }
+ mLastFrameTime = mFrameStartTime;
+ }
+ }
+
+ public void updateZoomedView(final ByteBuffer data) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (LayerView.ZoomedViewListener listener : mZoomedViewListeners) {
+ data.position(0);
+ listener.updateView(data);
+ }
+ }
+ });
+ }
+
+ public void addZoomedViewListener(LayerView.ZoomedViewListener listener) {
+ ThreadUtils.assertOnUiThread();
+ mZoomedViewListeners.add(listener);
+ }
+
+ public void removeZoomedViewListener(LayerView.ZoomedViewListener listener) {
+ ThreadUtils.assertOnUiThread();
+ mZoomedViewListeners.remove(listener);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -0,0 +1,721 @@
+/* -*- 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.gfx;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+
+import org.mozilla.gecko.AndroidGamepadManager;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAccessibility;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.GeckoThread;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.InputDevice;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+
+/**
+ * A view rendered by the layer compositor.
+ */
+public class LayerView extends ScrollView {
+ private static final String LOGTAG = "GeckoLayerView";
+
+ private GeckoLayerClient mLayerClient;
+ private PanZoomController mPanZoomController;
+ private DynamicToolbarAnimator mToolbarAnimator;
+ private GLController mGLController;
+ private LayerRenderer mRenderer;
+ /* Must be a PAINT_xxx constant */
+ private int mPaintState;
+ private int mBackgroundColor;
+ private FullScreenState mFullScreenState;
+
+ private SurfaceView mSurfaceView;
+ private TextureView mTextureView;
+ private View mFillerView;
+
+ private Listener mListener;
+
+ private PointF mInitialTouchPoint;
+
+ private float mSurfaceTranslation;
+
+ /* This should only be modified on the Java UI thread. */
+ private final Overscroll mOverscroll;
+
+ /* Flags used to determine when to show the painted surface. */
+ public static final int PAINT_START = 0;
+ public static final int PAINT_BEFORE_FIRST = 1;
+ public static final int PAINT_AFTER_FIRST = 2;
+
+ public boolean shouldUseTextureView() {
+ // Disable TextureView support for now as it causes panning/zooming
+ // performance regressions (see bug 792259). Uncomment the code below
+ // once this bug is fixed.
+ return false;
+
+ /*
+ // we can only use TextureView on ICS or higher
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ Log.i(LOGTAG, "Not using TextureView: not on ICS+");
+ return false;
+ }
+
+ try {
+ // and then we can only use it if we have a hardware accelerated window
+ Method m = View.class.getMethod("isHardwareAccelerated", (Class[]) null);
+ return (Boolean) m.invoke(this);
+ } catch (Exception e) {
+ Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString());
+ return false;
+ } */
+ }
+
+ public LayerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mPaintState = PAINT_START;
+ mBackgroundColor = Color.WHITE;
+ mFullScreenState = FullScreenState.NONE;
+
+ if (Versions.feature14Plus) {
+ mOverscroll = new OverscrollEdgeEffect(this);
+ } else {
+ mOverscroll = null;
+ }
+ }
+
+ public LayerView(Context context) {
+ this(context, null);
+ }
+
+ public void initializeView(EventDispatcher eventDispatcher) {
+ mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher);
+ if (mOverscroll != null) {
+ mLayerClient.setOverscrollHandler(mOverscroll);
+ }
+
+ mPanZoomController = mLayerClient.getPanZoomController();
+ mToolbarAnimator = mLayerClient.getDynamicToolbarAnimator();
+
+ mRenderer = new LayerRenderer(this);
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+
+ GeckoAccessibility.setDelegate(this);
+ GeckoAccessibility.setAccessibilityManagerListeners(getContext());
+ }
+
+ /**
+ * MotionEventHelper dragAsync() robocop tests can instruct
+ * PanZoomController not to generate longpress events.
+ */
+ public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+ mPanZoomController.setIsLongpressEnabled(isLongpressEnabled);
+ }
+
+ private static Point getEventRadius(MotionEvent event) {
+ return new Point((int)event.getToolMajor() / 2,
+ (int)event.getToolMinor() / 2);
+ }
+
+ private boolean sendEventToGecko(MotionEvent event) {
+ if (!mLayerClient.isGeckoReady()) {
+ return false;
+ }
+
+ int action = event.getActionMasked();
+ PointF point = new PointF(event.getX(), event.getY());
+ if (action == MotionEvent.ACTION_DOWN) {
+ mInitialTouchPoint = point;
+ }
+
+ if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) {
+ Point p = getEventRadius(event);
+
+ if (PointUtils.subtract(point, mInitialTouchPoint).length() <
+ Math.max(PanZoomController.CLICK_THRESHOLD, Math.min(Math.min(p.x, p.y), PanZoomController.PAN_THRESHOLD))) {
+ // Don't send the touchmove event if if the users finger hasn't moved far.
+ // Necessary for Google Maps to work correctly. See bug 771099.
+ return true;
+ } else {
+ mInitialTouchPoint = null;
+ }
+ }
+
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event, false));
+ return true;
+ }
+
+ public void showSurface() {
+ // Fix this if TextureView support is turned back on above
+ mSurfaceView.setVisibility(View.VISIBLE);
+ }
+
+ public void hideSurface() {
+ // Fix this if TextureView support is turned back on above
+ mSurfaceView.setVisibility(View.INVISIBLE);
+ }
+
+ public void destroy() {
+ if (mLayerClient != null) {
+ mLayerClient.destroy();
+ }
+ if (mRenderer != null) {
+ mRenderer.destroy();
+ }
+ if (mGLController != null) {
+ if (mGLController.mView == this) {
+ mGLController.mView = null;
+ }
+ mGLController = null;
+ }
+ }
+
+ @Override
+ public void dispatchDraw(final Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ // We must have a layer client to get valid viewport metrics
+ if (mLayerClient != null && mOverscroll != null) {
+ mOverscroll.draw(canvas, getViewportMetrics());
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ requestFocus();
+ }
+ event.offsetLocation(0, -mSurfaceTranslation);
+
+ if (mToolbarAnimator != null && mToolbarAnimator.onInterceptTouchEvent(event)) {
+ if (mPanZoomController != null) {
+ mPanZoomController.onMotionEventVelocity(event.getEventTime(), mToolbarAnimator.getVelocity());
+ }
+ return true;
+ }
+ if (AppConstants.MOZ_ANDROID_APZ && !mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) {
+ return true;
+ }
+ return sendEventToGecko(event);
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ // If we get a touchscreen hover event, and accessibility is not enabled,
+ // don't send it to gecko.
+ if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN &&
+ !GeckoAccessibility.isEnabled()) {
+ return false;
+ }
+
+ event.offsetLocation(0, -mSurfaceTranslation);
+
+ if (AppConstants.MOZ_ANDROID_APZ) {
+ if (!mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ } else if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
+ return true;
+ }
+ }
+
+ return sendEventToGecko(event);
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ event.offsetLocation(0, -mSurfaceTranslation);
+
+ if (AndroidGamepadManager.handleMotionEvent(event)) {
+ return true;
+ }
+ if (AppConstants.MOZ_ANDROID_APZ && !mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // We are adding descendants to this LayerView, but we don't want the
+ // descendants to affect the way LayerView retains its focus.
+ setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
+
+ // This check should not be done before the view is attached to a window
+ // as hardware acceleration will not be enabled at that point.
+ // We must create and add the SurfaceView instance before the view tree
+ // is fully created to avoid flickering (see bug 801477).
+ if (shouldUseTextureView()) {
+ mTextureView = new TextureView(getContext());
+ mTextureView.setSurfaceTextureListener(new SurfaceTextureListener());
+
+ // The background is set to this color when the LayerView is
+ // created, and it will be shown immediately at startup. Shortly
+ // after, the tab's background color will be used before any content
+ // is shown.
+ mTextureView.setBackgroundColor(Color.WHITE);
+ addView(mTextureView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ } else {
+ // This will stop PropertyAnimator from creating a drawing cache (i.e. a bitmap)
+ // from a SurfaceView, which is just not possible (the bitmap will be transparent).
+ setWillNotCacheDrawing(false);
+
+ mSurfaceView = new LayerSurfaceView(getContext(), this);
+ mSurfaceView.setBackgroundColor(Color.WHITE);
+
+ // The "filler" view sits behind the URL bar and should never be
+ // visible. It exists solely to make this LayerView actually
+ // scrollable so that we can shift the surface around on the screen.
+ // Once we drop support for pre-Honeycomb Android versions this
+ // should not be needed; we can just turn LayerView back into a
+ // FrameLayout that holds mSurfaceView and nothing else.
+ mFillerView = new View(getContext()) {
+ @Override protected void onMeasure(int aWidthSpec, int aHeightSpec) {
+ setMeasuredDimension(0, Math.round(mToolbarAnimator.getMaxTranslation()));
+ }
+ };
+ mFillerView.setBackgroundColor(Color.RED);
+
+ LinearLayout container = new LinearLayout(getContext());
+ container.setOrientation(LinearLayout.VERTICAL);
+ container.addView(mFillerView);
+ container.addView(mSurfaceView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ addView(container, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+ SurfaceHolder holder = mSurfaceView.getHolder();
+ holder.addCallback(new SurfaceListener());
+ }
+ }
+
+ // Don't expose GeckoLayerClient to things outside this package; only expose it as an Object
+ GeckoLayerClient getLayerClient() { return mLayerClient; }
+
+ public PanZoomController getPanZoomController() { return mPanZoomController; }
+ public DynamicToolbarAnimator getDynamicToolbarAnimator() { return mToolbarAnimator; }
+
+ public ImmutableViewportMetrics getViewportMetrics() {
+ return mLayerClient.getViewportMetrics();
+ }
+
+ public void abortPanning() {
+ if (mPanZoomController != null) {
+ mPanZoomController.abortPanning();
+ }
+ }
+
+ public PointF convertViewPointToLayerPoint(PointF viewPoint) {
+ return mLayerClient.convertViewPointToLayerPoint(viewPoint);
+ }
+
+ int getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ @Override
+ public void setBackgroundColor(int newColor) {
+ mBackgroundColor = newColor;
+ requestRender();
+ }
+
+ public void setSurfaceBackgroundColor(int newColor) {
+ if (mSurfaceView != null) {
+ mSurfaceView.setBackgroundColor(newColor);
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (AppConstants.MOZ_ANDROID_APZ && !mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onKeyEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ public void requestRender() {
+ if (mListener != null) {
+ mListener.renderRequested();
+ }
+ }
+
+ public void addLayer(Layer layer) {
+ mRenderer.addLayer(layer);
+ }
+
+ public void removeLayer(Layer layer) {
+ mRenderer.removeLayer(layer);
+ }
+
+ public void postRenderTask(RenderTask task) {
+ mRenderer.postRenderTask(task);
+ }
+
+ public void removeRenderTask(RenderTask task) {
+ mRenderer.removeRenderTask(task);
+ }
+
+ public int getMaxTextureSize() {
+ return mRenderer.getMaxTextureSize();
+ }
+
+ /** Used by robocop for testing purposes. Not for production use! */
+ @RobocopTarget
+ public IntBuffer getPixels() {
+ return mRenderer.getPixels();
+ }
+
+ /* paintState must be a PAINT_xxx constant. */
+ public void setPaintState(int paintState) {
+ mPaintState = paintState;
+ }
+
+ public int getPaintState() {
+ return mPaintState;
+ }
+
+ public LayerRenderer getRenderer() {
+ return mRenderer;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ Listener getListener() {
+ return mListener;
+ }
+
+ public void setGLController(final GLController glController) {
+ mGLController = glController;
+ glController.mView = this;
+
+ final NativePanZoomController npzc = AppConstants.MOZ_ANDROID_APZ ?
+ (NativePanZoomController) mPanZoomController : null;
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ glController.attachToJava(mLayerClient, npzc);
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ glController, "attachToJava",
+ GeckoLayerClient.class, mLayerClient,
+ NativePanZoomController.class, npzc);
+ }
+ }
+
+ public GLController getGLController() {
+ return mGLController;
+ }
+
+ /* When using a SurfaceView (mSurfaceView != null), resizing happens in two
+ * phases. First, the LayerView changes size, then, often some frames later,
+ * the SurfaceView changes size. Because of this, we need to split the
+ * resize into two phases to avoid jittering.
+ *
+ * The first phase is the LayerView size change. mListener is notified so
+ * that a synchronous draw can be performed (otherwise a blank frame will
+ * appear).
+ *
+ * The second phase is the SurfaceView size change. At this point, the
+ * backing GL surface is resized and another synchronous draw is performed.
+ * Gecko is also sent the new window size, and this will likely cause an
+ * extra draw a few frames later, after it's re-rendered and caught up.
+ *
+ * In the case that there is no valid GL surface (for example, when
+ * resuming, or when coming back from the awesomescreen), or we're using a
+ * TextureView instead of a SurfaceView, the first phase is skipped.
+ */
+ private void onSizeChanged(int width, int height) {
+ if (!mGLController.isServerSurfaceValid() || mSurfaceView == null) {
+ surfaceChanged(width, height);
+ return;
+ }
+
+ if (mListener != null) {
+ mListener.sizeChanged(width, height);
+ }
+
+ if (mOverscroll != null) {
+ mOverscroll.setSize(width, height);
+ }
+ }
+
+ private void surfaceChanged(int width, int height) {
+ mGLController.serverSurfaceChanged(width, height);
+
+ if (mListener != null) {
+ mListener.surfaceChanged(width, height);
+ }
+
+ if (mOverscroll != null) {
+ mOverscroll.setSize(width, height);
+ }
+ }
+
+ private void onDestroyed() {
+ mGLController.serverSurfaceDestroyed();
+ }
+
+ public Object getNativeWindow() {
+ if (mSurfaceView != null)
+ return mSurfaceView.getHolder();
+
+ return mTextureView.getSurfaceTexture();
+ }
+
+ public Object getSurface() {
+ if (mSurfaceView != null) {
+ return mSurfaceView.getHolder().getSurface();
+ }
+ return null;
+ }
+
+ //This method is called on the Gecko main thread.
+ @WrapForJNI(allowMultithread = true, stubName = "updateZoomedView")
+ public static void updateZoomedView(ByteBuffer data) {
+ LayerView layerView = GeckoAppShell.getLayerView();
+ if (layerView != null) {
+ LayerRenderer layerRenderer = layerView.getRenderer();
+ if (layerRenderer != null) {
+ layerRenderer.updateZoomedView(data);
+ }
+ }
+ }
+
+ public interface Listener {
+ void renderRequested();
+ void sizeChanged(int width, int height);
+ void surfaceChanged(int width, int height);
+ }
+
+ private class SurfaceListener implements SurfaceHolder.Callback {
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ onDestroyed();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int aWidthSpec, int aHeightSpec) {
+ super.onMeasure(aWidthSpec, aHeightSpec);
+ if (mSurfaceView != null) {
+ // Because of the crazy setup where this LayerView is a ScrollView
+ // and the SurfaceView is inside a LinearLayout, the SurfaceView
+ // doesn't get the right information to size itself the way we want.
+ // We always want it to be the same size as this LayerView, so we
+ // use a hack to make sure it sizes itself that way.
+ ((LayerSurfaceView)mSurfaceView).overrideSize(getMeasuredWidth(), getMeasuredHeight());
+ }
+ }
+
+ /* A subclass of SurfaceView to listen to layout changes, as
+ * View.OnLayoutChangeListener requires API level 11.
+ */
+ private class LayerSurfaceView extends SurfaceView {
+ private LayerView mParent;
+ private int mForcedWidth;
+ private int mForcedHeight;
+
+ public LayerSurfaceView(Context aContext, LayerView aParent) {
+ super(aContext);
+ mParent = aParent;
+ }
+
+ void overrideSize(int aWidth, int aHeight) {
+ if (mForcedWidth != aWidth || mForcedHeight != aHeight) {
+ mForcedWidth = aWidth;
+ mForcedHeight = aHeight;
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int aWidthSpec, int aHeightSpec) {
+ setMeasuredDimension(mForcedWidth, mForcedHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed && mParent.mGLController.isServerSurfaceValid()) {
+ mParent.surfaceChanged(right - left, bottom - top);
+ }
+ }
+ }
+
+ private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ // We don't do this for surfaceCreated above because it is always followed by a surfaceChanged,
+ // but that is not the case here.
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ onDestroyed();
+ return true; // allow Android to call release() on the SurfaceTexture, we are done drawing to it
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+
+ }
+ }
+
+ @RobocopTarget
+ public void addDrawListener(DrawListener listener) {
+ mLayerClient.addDrawListener(listener);
+ }
+
+ @RobocopTarget
+ public void removeDrawListener(DrawListener listener) {
+ mLayerClient.removeDrawListener(listener);
+ }
+
+ @RobocopTarget
+ public static interface DrawListener {
+ public void drawFinished();
+ }
+
+ @Override
+ public void setOverScrollMode(int overscrollMode) {
+ super.setOverScrollMode(overscrollMode);
+ if (mPanZoomController != null) {
+ mPanZoomController.setOverScrollMode(overscrollMode);
+ }
+ }
+
+ @Override
+ public int getOverScrollMode() {
+ if (mPanZoomController != null) {
+ return mPanZoomController.getOverScrollMode();
+ }
+
+ return super.getOverScrollMode();
+ }
+
+ public float getZoomFactor() {
+ return getLayerClient().getViewportMetrics().zoomFactor;
+ }
+
+ @Override
+ public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ GeckoAccessibility.onLayerViewFocusChanged(this, gainFocus);
+ }
+
+ public void setFullScreenState(FullScreenState state) {
+ mFullScreenState = state;
+ }
+
+ public boolean isFullScreen() {
+ return mFullScreenState != FullScreenState.NONE;
+ }
+
+ public FullScreenState getFullScreenState() {
+ return mFullScreenState;
+ }
+
+ public void setMaxTranslation(float aMaxTranslation) {
+ mToolbarAnimator.setMaxTranslation(aMaxTranslation);
+ if (mFillerView != null) {
+ mFillerView.requestLayout();
+ }
+ }
+
+ public void setSurfaceTranslation(float translation) {
+ // Once we drop support for pre-Honeycomb Android versions, we can
+ // revert bug 1197811 and just use ViewHelper here.
+ if (mSurfaceTranslation != translation) {
+ mSurfaceTranslation = translation;
+ scrollTo(0, Math.round(mToolbarAnimator.getMaxTranslation() - translation));
+ }
+ }
+
+ public float getSurfaceTranslation() {
+ return mSurfaceTranslation;
+ }
+
+ // Public hooks for dynamic toolbar translation
+
+ public interface DynamicToolbarListener {
+ public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation);
+ public void onPanZoomStopped();
+ public void onMetricsChanged(ImmutableViewportMetrics viewport);
+ }
+
+ // Public hooks for zoomed view
+
+ public interface ZoomedViewListener {
+ public void requestZoomedViewRender();
+ public void updateView(ByteBuffer data);
+ }
+
+ public void addZoomedViewListener(ZoomedViewListener listener) {
+ mRenderer.addZoomedViewListener(listener);
+ }
+
+ public void removeZoomedViewListener(ZoomedViewListener listener) {
+ mRenderer.removeZoomedViewListener(listener);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
@@ -0,0 +1,348 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONObject;
+
+import android.graphics.PointF;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.InputDevice;
+
+class NativePanZoomController extends JNIObject implements PanZoomController {
+ private final PanZoomTarget mTarget;
+ private final LayerView mView;
+ private boolean mDestroyed;
+ private Overscroll mOverscroll;
+ boolean mNegateWheelScroll;
+ private float mPointerScrollFactor;
+ private final PrefsHelper.PrefHandler mPrefsObserver;
+ private long mLastDownTime;
+ private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
+
+ @WrapForJNI
+ private native boolean handleMotionEvent(
+ int action, int actionIndex, long time, int metaState,
+ int pointerId[], float x[], float y[], float orientation[], float pressure[],
+ float toolMajor[], float toolMinor[]);
+
+ @WrapForJNI
+ private native boolean handleScrollEvent(
+ long time, int metaState,
+ float x, float y,
+ float hScroll, float vScroll);
+
+ @WrapForJNI
+ private native boolean handleMouseEvent(
+ int action, long time, int metaState,
+ float x, float y, int buttons);
+
+ @WrapForJNI
+ private native void handleMotionEventVelocity(long time, float ySpeed);
+
+ private boolean handleMotionEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int action = event.getActionMasked();
+ final int count = event.getPointerCount();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mLastDownTime = event.getDownTime();
+ } else if (mLastDownTime != event.getDownTime()) {
+ return false;
+ }
+
+ final int[] pointerId = new int[count];
+ final float[] x = new float[count];
+ final float[] y = new float[count];
+ final float[] orientation = new float[count];
+ final float[] pressure = new float[count];
+ final float[] toolMajor = new float[count];
+ final float[] toolMinor = new float[count];
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+
+ for (int i = 0; i < count; i++) {
+ pointerId[i] = event.getPointerId(i);
+ event.getPointerCoords(i, coords);
+
+ x[i] = coords.x;
+ y[i] = coords.y;
+
+ orientation[i] = coords.orientation;
+ pressure[i] = coords.pressure;
+
+ // If we are converting to CSS pixels, we should adjust the radii as well.
+ toolMajor[i] = coords.toolMajor;
+ toolMinor[i] = coords.toolMinor;
+ }
+
+ return handleMotionEvent(action, event.getActionIndex(), event.getEventTime(),
+ event.getMetaState(), pointerId, x, y, orientation, pressure,
+ toolMajor, toolMinor);
+ }
+
+ private boolean handleScrollEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int count = event.getPointerCount();
+
+ if (count <= 0) {
+ return false;
+ }
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ event.getPointerCoords(0, coords);
+ final float x = coords.x;
+ final float y = coords.y;
+
+ final float flipFactor = mNegateWheelScroll ? -1.0f : 1.0f;
+ final float hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) * flipFactor * mPointerScrollFactor;
+ final float vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * flipFactor * mPointerScrollFactor;
+
+ return handleScrollEvent(event.getEventTime(), event.getMetaState(), x, y, hScroll, vScroll);
+ }
+
+ private boolean handleMouseEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int count = event.getPointerCount();
+
+ if (count <= 0) {
+ return false;
+ }
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ event.getPointerCoords(0, coords);
+ final float x = coords.x;
+ final float y = coords.y;
+
+ return handleMouseEvent(event.getActionMasked(), event.getEventTime(), event.getMetaState(), x, y, event.getButtonState());
+ }
+
+
+ NativePanZoomController(PanZoomTarget target, View view) {
+ mTarget = target;
+ mView = (LayerView) view;
+
+ String[] prefs = { "ui.scrolling.negate_wheel_scroll" };
+ mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, boolean value) {
+ if (pref.equals("ui.scrolling.negate_wheel_scroll")) {
+ mNegateWheelScroll = value;
+ }
+ }
+ };
+ PrefsHelper.addObserver(prefs, mPrefsObserver);
+
+ TypedValue outValue = new TypedValue();
+ if (view.getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
+ mPointerScrollFactor = outValue.getDimension(view.getContext().getResources().getDisplayMetrics());
+ } else {
+ mPointerScrollFactor = MAX_SCROLL;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
+ return handleMouseEvent(event);
+ } else {
+ return handleMotionEvent(event);
+ }
+ }
+
+ @Override
+ public boolean onMotionEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_SCROLL) {
+ if (event.getDownTime() >= mLastDownTime) {
+ mLastDownTime = event.getDownTime();
+ } else if ((InputDevice.getDevice(event.getDeviceId()).getSources() & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
+ return false;
+ }
+ return handleScrollEvent(event);
+ } else if ((action == MotionEvent.ACTION_HOVER_MOVE) ||
+ (action == MotionEvent.ACTION_HOVER_ENTER) ||
+ (action == MotionEvent.ACTION_HOVER_EXIT)) {
+ return handleMouseEvent(event);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onKeyEvent(KeyEvent event) {
+ // FIXME implement this
+ return false;
+ }
+
+ @Override
+ public void onMotionEventVelocity(final long aEventTime, final float aSpeedY) {
+ handleMotionEventVelocity(aEventTime, aSpeedY);
+ }
+
+ @Override
+ public PointF getVelocityVector() {
+ // FIXME implement this
+ return new PointF(0, 0);
+ }
+
+ @Override
+ public void pageRectUpdated() {
+ // no-op in APZC, I think
+ }
+
+ @Override
+ public void abortPanning() {
+ // no-op in APZC, I think
+ }
+
+ @Override
+ public void notifyDefaultActionPrevented(boolean prevented) {
+ // no-op: This could get called if accessibility is enabled and the events
+ // are sent to Gecko directly without going through APZ. In this case
+ // we just want to ignore this callback.
+ }
+
+ @WrapForJNI(stubName = "AbortAnimation")
+ private native void nativeAbortAnimation();
+
+ @Override // PanZoomController
+ public void abortAnimation()
+ {
+ if (!mDestroyed) {
+ nativeAbortAnimation();
+ }
+ }
+
+ @Override // PanZoomController
+ public boolean getRedrawHint()
+ {
+ // FIXME implement this
+ return true;
+ }
+
+ @Override @WrapForJNI(allowMultithread = true) // PanZoomController
+ public void destroy() {
+ if (mDestroyed) {
+ return;
+ }
+ mDestroyed = true;
+ disposeNative();
+ }
+
+ @Override @WrapForJNI // JNIObject
+ protected native void disposeNative();
+
+ @Override
+ public void setOverScrollMode(int overscrollMode) {
+ // FIXME implement this
+ }
+
+ @Override
+ public int getOverScrollMode() {
+ // FIXME implement this
+ return 0;
+ }
+
+ @WrapForJNI(allowMultithread = true, stubName = "RequestContentRepaintWrapper")
+ private void requestContentRepaint(float x, float y, float width, float height, float resolution) {
+ mTarget.forceRedraw(new DisplayPortMetrics(x, y, x + width, y + height, resolution));
+ }
+
+ @Override
+ public void setOverscrollHandler(final Overscroll handler) {
+ mOverscroll = handler;
+ }
+
+ @WrapForJNI(stubName = "SetIsLongpressEnabled")
+ private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
+
+ @Override // PanZoomController
+ public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+ if (!mDestroyed) {
+ nativeSetIsLongpressEnabled(isLongpressEnabled);
+ }
+ }
+
+ @WrapForJNI(stubName = "AdjustScrollForSurfaceShift")
+ private native void adjustScrollForSurfaceShift(float aX, float aY);
+
+ @Override // PanZoomController
+ public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift) {
+ adjustScrollForSurfaceShift(aShift.x, aShift.y);
+ return aMetrics.offsetViewportByAndClamp(aShift.x, aShift.y);
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ private void updateOverscrollVelocity(final float x, final float y) {
+ if (mOverscroll != null) {
+ if (ThreadUtils.isOnUiThread() == true) {
+ mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
+ mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
+ } else {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Multiply the velocity by 1000 to match what was done in JPZ.
+ mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
+ mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
+ }
+ });
+ }
+ }
+ }
+
+ @WrapForJNI(allowMultithread = true)
+ private void updateOverscrollOffset(final float x, final float y) {
+ if (mOverscroll != null) {
+ if (ThreadUtils.isOnUiThread() == true) {
+ mOverscroll.setDistance(x, Overscroll.Axis.X);
+ mOverscroll.setDistance(y, Overscroll.Axis.Y);
+ } else {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mOverscroll.setDistance(x, Overscroll.Axis.X);
+ mOverscroll.setDistance(y, Overscroll.Axis.Y);
+ }
+ });
+ }
+ }
+ }
+
+ @WrapForJNI
+ private void setScrollingRootContent(final boolean isRootContent) {
+ mTarget.setScrollingRootContent(isRootContent);
+ }
+
+ /**
+ * Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
+ * to avoid unwanted scroll interactions.
+ */
+ @WrapForJNI
+ private void onSelectionDragState(boolean state) {
+ mView.getDynamicToolbarAnimator().setPinned(state, PinReason.CARET_DRAG);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
@@ -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.gfx;
+
+import android.graphics.Canvas;
+
+public interface Overscroll {
+ // The axis to show overscroll on.
+ public enum Axis {
+ X,
+ Y,
+ };
+
+ public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics);
+ public void setSize(final int width, final int height);
+ public void setVelocity(final float velocity, final Axis axis);
+ public void setDistance(final float distance, final Axis axis);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
@@ -0,0 +1,163 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.widget.EdgeEffect;
+
+import java.lang.reflect.Field;
+
+public class OverscrollEdgeEffect implements Overscroll {
+ // Used to index particular edges in the edges array
+ private static final int TOP = 0;
+ private static final int BOTTOM = 1;
+ private static final int LEFT = 2;
+ private static final int RIGHT = 3;
+
+ // All four edges of the screen
+ private final EdgeEffect[] mEdges = new EdgeEffect[4];
+
+ // The view we're showing this overscroll on.
+ private final LayerView mView;
+
+ public OverscrollEdgeEffect(final LayerView v) {
+ Field paintField = null;
+ if (Versions.feature21Plus) {
+ try {
+ paintField = EdgeEffect.class.getDeclaredField("mPaint");
+ paintField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ }
+ }
+
+ mView = v;
+ Context context = v.getContext();
+ for (int i = 0; i < 4; i++) {
+ mEdges[i] = new EdgeEffect(context);
+
+ try {
+ if (paintField != null) {
+ final Paint p = (Paint) paintField.get(mEdges[i]);
+
+ // The Android EdgeEffect class uses a mode of SRC_ATOP here, which means it will only
+ // draw the effect where there are non-transparent pixels in the destination. Since the LayerView
+ // itself is fully transparent, it doesn't display at all. We need to use SRC instead.
+ p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ }
+ } catch (IllegalAccessException e) {
+ }
+ }
+ }
+
+ @Override
+ public void setSize(final int width, final int height) {
+ mEdges[LEFT].setSize(height, width);
+ mEdges[RIGHT].setSize(height, width);
+ mEdges[TOP].setSize(width, height);
+ mEdges[BOTTOM].setSize(width, height);
+ }
+
+ private EdgeEffect getEdgeForAxisAndSide(final Axis axis, final float side) {
+ if (axis == Axis.Y) {
+ if (side < 0) {
+ return mEdges[TOP];
+ } else {
+ return mEdges[BOTTOM];
+ }
+ } else {
+ if (side < 0) {
+ return mEdges[LEFT];
+ } else {
+ return mEdges[RIGHT];
+ }
+ }
+ }
+
+ private void invalidate() {
+ if (Versions.feature16Plus) {
+ mView.postInvalidateOnAnimation();
+ } else {
+ mView.postInvalidateDelayed(10);
+ }
+ }
+
+ @Override
+ public void setVelocity(final float velocity, final Axis axis) {
+ final EdgeEffect edge = getEdgeForAxisAndSide(axis, velocity);
+
+ // If we're showing overscroll already, start fading it out.
+ if (!edge.isFinished()) {
+ edge.onRelease();
+ } else {
+ // Otherwise, show an absorb effect
+ edge.onAbsorb((int)velocity);
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public void setDistance(final float distance, final Axis axis) {
+ // The first overscroll event often has zero distance. Throw it out
+ if (distance == 0.0f) {
+ return;
+ }
+
+ final EdgeEffect edge = getEdgeForAxisAndSide(axis, (int)distance);
+ edge.onPull(distance / (axis == Axis.X ? mView.getWidth() : mView.getHeight()));
+ invalidate();
+ }
+
+ @Override
+ public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics) {
+ if (metrics == null) {
+ return;
+ }
+
+ float fillerSize = mView.getDynamicToolbarAnimator().getMaxTranslation();
+ PointF visibleEnd = mView.getDynamicToolbarAnimator().getVisibleEndOfLayerView();
+
+ // If we're pulling an edge, or fading it out, draw!
+ boolean invalidate = false;
+ if (!mEdges[TOP].isFinished()) {
+ invalidate |= draw(mEdges[TOP], canvas, 0, fillerSize, 0);
+ }
+
+ if (!mEdges[BOTTOM].isFinished()) {
+ invalidate |= draw(mEdges[BOTTOM], canvas, visibleEnd.x, fillerSize + visibleEnd.y, 180);
+ }
+
+ if (!mEdges[LEFT].isFinished()) {
+ invalidate |= draw(mEdges[LEFT], canvas, 0, fillerSize + visibleEnd.y, 270);
+ }
+
+ if (!mEdges[RIGHT].isFinished()) {
+ invalidate |= draw(mEdges[RIGHT], canvas, visibleEnd.x, fillerSize, 90);
+ }
+
+ // If the edge effect is animating off screen, invalidate.
+ if (invalidate) {
+ invalidate();
+ }
+ }
+
+ private static boolean draw(final EdgeEffect edge, final Canvas canvas, final float translateX, final float translateY, final float rotation) {
+ final int state = canvas.save();
+ canvas.translate(translateX, translateY);
+ canvas.rotate(rotation);
+ boolean invalidate = edge.draw(canvas);
+ canvas.restoreToCount(state);
+
+ return invalidate;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -0,0 +1,56 @@
+/* -*- 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.gfx;
+
+import android.graphics.PointF;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+
+public interface PanZoomController {
+ // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
+ // between the touch-down and touch-up of a click). In units of density-independent pixels.
+ public static final float PAN_THRESHOLD = 1 / 16f * GeckoAppShell.getDpi();
+
+ // Threshold for sending touch move events to content
+ public static final float CLICK_THRESHOLD = 1 / 50f * GeckoAppShell.getDpi();
+
+ static class Factory {
+ static PanZoomController create(PanZoomTarget target, View view, EventDispatcher dispatcher) {
+ if (org.mozilla.gecko.AppConstants.MOZ_ANDROID_APZ) {
+ return new NativePanZoomController(target, view);
+ } else {
+ throw new IllegalStateException("Must set MOZ_ANDROID_APZ");
+ }
+ }
+ }
+
+ public void destroy();
+
+ public boolean onTouchEvent(MotionEvent event);
+ public boolean onMotionEvent(MotionEvent event);
+ public boolean onKeyEvent(KeyEvent event);
+ public void onMotionEventVelocity(final long aEventTime, final float aSpeedY);
+ public void notifyDefaultActionPrevented(boolean prevented);
+
+ public boolean getRedrawHint();
+ public PointF getVelocityVector();
+
+ public void pageRectUpdated();
+ public void abortPanning();
+ public void abortAnimation();
+
+ public void setOverScrollMode(int overscrollMode);
+ public int getOverScrollMode();
+
+ public void setOverscrollHandler(final Overscroll controller);
+
+ public void setIsLongpressEnabled(boolean isLongpressEnabled);
+
+ public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java
@@ -0,0 +1,29 @@
+/* -*- 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.gfx;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+public interface PanZoomTarget {
+ public ImmutableViewportMetrics getViewportMetrics();
+ public FullScreenState getFullScreenState();
+ public PointF getVisibleEndOfLayerView();
+
+ public void setAnimationTarget(ImmutableViewportMetrics viewport);
+ public void setViewportMetrics(ImmutableViewportMetrics viewport);
+ public void scrollBy(float dx, float dy);
+ public void panZoomStopped();
+ /** This triggers an (asynchronous) viewport update/redraw. */
+ public void forceRedraw(DisplayPortMetrics displayPort);
+
+ public boolean post(Runnable action);
+ public void postRenderTask(RenderTask task);
+ public void removeRenderTask(RenderTask task);
+ public Object getLock();
+ public PointF convertViewPointToLayerPoint(PointF viewPoint);
+ public void setScrollingRootContent(boolean isRootContent);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
@@ -0,0 +1,127 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PanningPerfAPI {
+ private static final String LOGTAG = "GeckoPanningPerfAPI";
+
+ // make this large enough to avoid having to resize the frame time
+ // list, as that may be expensive and impact the thing we're trying
+ // to measure.
+ private static final int EXPECTED_FRAME_COUNT = 2048;
+
+ private static boolean mRecordingFrames;
+ private static List<Long> mFrameTimes;
+ private static long mFrameStartTime;
+
+ private static boolean mRecordingCheckerboard;
+ private static List<Float> mCheckerboardAmounts;
+ private static long mCheckerboardStartTime;
+
+ private static void initialiseRecordingArrays() {
+ if (mFrameTimes == null) {
+ mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
+ } else {
+ mFrameTimes.clear();
+ }
+ if (mCheckerboardAmounts == null) {
+ mCheckerboardAmounts = new ArrayList<Float>(EXPECTED_FRAME_COUNT);
+ } else {
+ mCheckerboardAmounts.clear();
+ }
+ }
+
+ @RobocopTarget
+ public static void startFrameTimeRecording() {
+ if (mRecordingFrames || mRecordingCheckerboard) {
+ Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
+ return;
+ }
+ mRecordingFrames = true;
+ initialiseRecordingArrays();
+ mFrameStartTime = SystemClock.uptimeMillis();
+ }
+
+ @RobocopTarget
+ public static List<Long> stopFrameTimeRecording() {
+ if (!mRecordingFrames) {
+ Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
+ return null;
+ }
+ mRecordingFrames = false;
+ return mFrameTimes;
+ }
+
+ public static void recordFrameTime() {
+ // this will be called often, so try to make it as quick as possible
+ if (mRecordingFrames) {
+ mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
+ }
+ }
+
+ public static boolean isRecordingCheckerboard() {
+ return mRecordingCheckerboard;
+ }
+
+ @RobocopTarget
+ public static void startCheckerboardRecording() {
+ if (mRecordingCheckerboard || mRecordingFrames) {
+ Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!");
+ return;
+ }
+ mRecordingCheckerboard = true;
+ initialiseRecordingArrays();
+ mCheckerboardStartTime = SystemClock.uptimeMillis();
+ }
+
+ @RobocopTarget
+ public static List<Float> stopCheckerboardRecording() {
+ if (!mRecordingCheckerboard) {
+ Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!");
+ return null;
+ }
+ mRecordingCheckerboard = false;
+
+ // We take the number of values in mCheckerboardAmounts here, as there's
+ // the possibility that this function is called while recordCheckerboard
+ // is still executing. As values are added to this list last, we use
+ // this number as the canonical number of recordings.
+ int values = mCheckerboardAmounts.size();
+ if (values == 0) {
+ Log.w(LOGTAG, "stopCheckerboardRecording() found no checkerboard amounts!");
+ return mCheckerboardAmounts;
+ }
+
+ // The score will be the sum of all the values in mCheckerboardAmounts,
+ // so weight the checkerboard values by time so that frame-rate and
+ // run-length don't affect score.
+ long lastTime = 0;
+ float totalTime = mFrameTimes.get(values - 1);
+ for (int i = 0; i < values; i++) {
+ long elapsedTime = mFrameTimes.get(i) - lastTime;
+ mCheckerboardAmounts.set(i, mCheckerboardAmounts.get(i) * elapsedTime / totalTime);
+ lastTime += elapsedTime;
+ }
+
+ return mCheckerboardAmounts;
+ }
+
+ public static void recordCheckerboard(float amount) {
+ // this will be called often, so try to make it as quick as possible
+ if (mRecordingCheckerboard) {
+ mFrameTimes.add(SystemClock.uptimeMillis() - mCheckerboardStartTime);
+ mCheckerboardAmounts.add(amount);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PluginLayer.java
@@ -0,0 +1,167 @@
+/* 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.gfx;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.AbsoluteLayout;
+
+public class PluginLayer extends Layer {
+ private static final String LOGTAG = "PluginLayer";
+
+ private final View mView;
+ private SurfaceView mSurfaceView;
+ private final PluginLayoutParams mLayoutParams;
+ private final AbsoluteLayout mContainer;
+
+ private boolean mDestroyed;
+ private boolean mViewVisible;
+
+ private RectF mLastViewport;
+ private float mLastZoomFactor;
+
+ private static final float TEXTURE_MAP[] = {
+ 0.0f, 1.0f, // top left
+ 0.0f, 0.0f, // bottom left
+ 1.0f, 1.0f, // top right
+ 1.0f, 0.0f, // bottom right
+ };
+
+ public PluginLayer(View view, RectF rect, int maxDimension) {
+ super(new IntSize(0, 0));
+
+ mView = view;
+ mContainer = GeckoAppShell.getGeckoInterface().getPluginContainer();
+
+ mView.setWillNotDraw(false);
+ if (mView instanceof SurfaceView) {
+ mSurfaceView = (SurfaceView)view;
+ mSurfaceView.setZOrderOnTop(false);
+ mSurfaceView.setZOrderMediaOverlay(true);
+ }
+
+ mLayoutParams = new PluginLayoutParams(rect, maxDimension);
+ }
+
+ public void setVisible(boolean visible) {
+ if (visible) {
+ showView();
+ } else {
+ hideView();
+ }
+ }
+
+ private void hideView() {
+ if (mViewVisible) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mView.setVisibility(View.GONE);
+ mViewVisible = false;
+ }
+ });
+ }
+ }
+
+ public void showView() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mContainer.indexOfChild(mView) < 0) {
+ mContainer.addView(mView, mLayoutParams);
+ } else {
+ mContainer.updateViewLayout(mView, mLayoutParams);
+ mView.setVisibility(View.VISIBLE);
+ }
+ mViewVisible = true;
+ }
+ });
+ }
+
+ public void destroy() {
+ mDestroyed = true;
+
+ mContainer.removeView(mView);
+ }
+
+ public void reset(RectF rect) {
+ mLayoutParams.reset(rect);
+ }
+
+ @Override
+ protected void performUpdates(RenderContext context) {
+ if (mDestroyed)
+ return;
+
+ if (!RectUtils.fuzzyEquals(context.viewport, mLastViewport) ||
+ !FloatUtils.fuzzyEquals(context.zoomFactor, mLastZoomFactor)) {
+
+ mLastZoomFactor = context.zoomFactor;
+ mLastViewport = context.viewport;
+ mLayoutParams.reposition(context.viewport, context.zoomFactor);
+
+ showView();
+ }
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ }
+
+ class PluginLayoutParams extends AbsoluteLayout.LayoutParams
+ {
+ private static final String LOGTAG = "GeckoApp.PluginLayoutParams";
+
+ private RectF mRect;
+ private final int mMaxDimension;
+ private float mLastResolution;
+
+ public PluginLayoutParams(RectF rect, int maxDimension) {
+ super(0, 0, 0, 0);
+
+ mMaxDimension = maxDimension;
+ reset(rect);
+ }
+
+ private void clampToMaxSize() {
+ if (width > mMaxDimension || height > mMaxDimension) {
+ if (width > height) {
+ height = Math.round(((float) height / width) * mMaxDimension);
+ width = mMaxDimension;
+ } else {
+ width = Math.round(((float) width / height) * mMaxDimension);
+ height = mMaxDimension;
+ }
+ }
+ }
+
+ public void reset(RectF rect) {
+ mRect = rect;
+ }
+
+ public void reposition(RectF viewport, float zoomFactor) {
+
+ RectF scaled = RectUtils.scale(mRect, zoomFactor);
+
+ this.x = Math.round(scaled.left - viewport.left);
+ this.y = Math.round(scaled.top - viewport.top);
+
+ if (!FloatUtils.fuzzyEquals(mLastResolution, zoomFactor)) {
+ width = Math.round(mRect.width() * zoomFactor);
+ height = Math.round(mRect.height() * zoomFactor);
+ mLastResolution = zoomFactor;
+
+ clampToMaxSize();
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java
@@ -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.gfx;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+
+public final class PointUtils {
+ public static PointF add(PointF one, PointF two) {
+ return new PointF(one.x + two.x, one.y + two.y);
+ }
+
+ public static PointF subtract(PointF one, PointF two) {
+ return new PointF(one.x - two.x, one.y - two.y);
+ }
+
+ public static PointF scale(PointF point, float factor) {
+ return new PointF(point.x * factor, point.y * factor);
+ }
+
+ public static Point round(PointF point) {
+ return new Point(Math.round(point.x), Math.round(point.y));
+ }
+
+ /* Computes the magnitude of the given vector. */
+ public static float distance(PointF point) {
+ return (float)Math.sqrt(point.x * point.x + point.y * point.y);
+ }
+
+ /** Computes the scalar distance between two points. */
+ public static float distance(PointF one, PointF two) {
+ return PointF.length(one.x - two.x, one.y - two.y);
+ }
+
+ public static JSONObject toJSON(PointF point) throws JSONException {
+ // Ensure we put ints, not longs, because Gecko message handlers call getInt().
+ int x = Math.round(point.x);
+ int y = Math.round(point.y);
+ JSONObject json = new JSONObject();
+ json.put("x", x);
+ json.put("y", y);
+ return json;
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
@@ -0,0 +1,29 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * This is the data structure that's returned by the progressive tile update
+ * callback function. It encompasses the current viewport and a boolean value
+ * representing whether the front-end is interested in the current progressive
+ * update continuing.
+ */
+@WrapForJNI
+public class ProgressiveUpdateData {
+ public float x;
+ public float y;
+ public float scale;
+ public boolean abort;
+
+ public void setViewport(ImmutableViewportMetrics viewport) {
+ this.x = viewport.viewportRectLeft;
+ this.y = viewport.viewportRectTop;
+ this.scale = viewport.zoomFactor;
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java
@@ -0,0 +1,126 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+public final class RectUtils {
+ private RectUtils() {}
+
+ public static Rect create(JSONObject json) {
+ try {
+ int x = json.getInt("x");
+ int y = json.getInt("y");
+ int width = json.getInt("width");
+ int height = json.getInt("height");
+ return new Rect(x, y, x + width, y + height);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String toJSON(RectF rect) {
+ StringBuilder sb = new StringBuilder(256);
+ sb.append("{ \"left\": ").append(rect.left)
+ .append(", \"top\": ").append(rect.top)
+ .append(", \"right\": ").append(rect.right)
+ .append(", \"bottom\": ").append(rect.bottom)
+ .append('}');
+ return sb.toString();
+ }
+
+ public static RectF expand(RectF rect, float moreWidth, float moreHeight) {
+ float halfMoreWidth = moreWidth / 2;
+ float halfMoreHeight = moreHeight / 2;
+ return new RectF(rect.left - halfMoreWidth,
+ rect.top - halfMoreHeight,
+ rect.right + halfMoreWidth,
+ rect.bottom + halfMoreHeight);
+ }
+
+ public static RectF contract(RectF rect, float lessWidth, float lessHeight) {
+ float halfLessWidth = lessWidth / 2.0f;
+ float halfLessHeight = lessHeight / 2.0f;
+ return new RectF(rect.left + halfLessWidth,
+ rect.top + halfLessHeight,
+ rect.right - halfLessWidth,
+ rect.bottom - halfLessHeight);
+ }
+
+ public static RectF intersect(RectF one, RectF two) {
+ float left = Math.max(one.left, two.left);
+ float top = Math.max(one.top, two.top);
+ float right = Math.min(one.right, two.right);
+ float bottom = Math.min(one.bottom, two.bottom);
+ return new RectF(left, top, Math.max(right, left), Math.max(bottom, top));
+ }
+
+ public static RectF scale(RectF rect, float scale) {
+ float x = rect.left * scale;
+ float y = rect.top * scale;
+ return new RectF(x, y,
+ x + (rect.width() * scale),
+ y + (rect.height() * scale));
+ }
+
+ public static RectF scaleAndRound(RectF rect, float scale) {
+ float left = rect.left * scale;
+ float top = rect.top * scale;
+ return new RectF(Math.round(left),
+ Math.round(top),
+ Math.round(left + (rect.width() * scale)),
+ Math.round(top + (rect.height() * scale)));
+ }
+
+ /** Returns the nearest integer rect of the given rect. */
+ public static Rect round(RectF rect) {
+ Rect r = new Rect();
+ round(rect, r);
+ return r;
+ }
+
+ public static void round(RectF rect, Rect dest) {
+ dest.set(Math.round(rect.left), Math.round(rect.top),
+ Math.round(rect.right), Math.round(rect.bottom));
+ }
+
+ public static Rect roundIn(RectF rect) {
+ return new Rect((int)Math.ceil(rect.left), (int)Math.ceil(rect.top),
+ (int)Math.floor(rect.right), (int)Math.floor(rect.bottom));
+ }
+
+ public static IntSize getSize(Rect rect) {
+ return new IntSize(rect.width(), rect.height());
+ }
+
+ public static Point getOrigin(Rect rect) {
+ return new Point(rect.left, rect.top);
+ }
+
+ public static PointF getOrigin(RectF rect) {
+ return new PointF(rect.left, rect.top);
+ }
+
+ public static boolean fuzzyEquals(RectF a, RectF b) {
+ if (a == null && b == null)
+ return true;
+ else if ((a == null && b != null) || (a != null && b == null))
+ return false;
+ else
+ return FloatUtils.fuzzyEquals(a.top, b.top)
+ && FloatUtils.fuzzyEquals(a.left, b.left)
+ && FloatUtils.fuzzyEquals(a.right, b.right)
+ && FloatUtils.fuzzyEquals(a.bottom, b.bottom);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java
@@ -0,0 +1,80 @@
+/* -*- 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.gfx;
+
+/**
+ * A class used to schedule a callback to occur when the next frame is drawn.
+ * Subclasses must redefine the internalRun method, not the run method.
+ */
+public abstract class RenderTask {
+ /**
+ * Whether to run the task after the render, or before.
+ */
+ public final boolean runAfter;
+
+ /**
+ * Time when this task has first run, in ns. Useful for tasks which run for a specific duration.
+ */
+ private long mStartTime;
+
+ /**
+ * Whether we should initialise mStartTime on the next frame run.
+ */
+ private boolean mResetStartTime = true;
+
+ /**
+ * The callback to run on each frame. timeDelta is the time elapsed since
+ * the last call, in nanoseconds. Returns true if it should continue
+ * running, or false if it should be removed from the task queue. Returning
+ * true implicitly schedules a redraw.
+ *
+ * This method first initializes the start time if resetStartTime has been invoked,
+ * then calls internalRun.
+ *
+ * Note : subclasses should override internalRun.
+ *
+ * @param timeDelta the time between the beginning of last frame and the beginning of this frame, in ns.
+ * @param currentFrameStartTime the startTime of the current frame, in ns.
+ * @return true if animation should be run at the next frame, false otherwise
+ * @see RenderTask#internalRun(long, long)
+ */
+ public final boolean run(long timeDelta, long currentFrameStartTime) {
+ if (mResetStartTime) {
+ mStartTime = currentFrameStartTime;
+ mResetStartTime = false;
+ }
+ return internalRun(timeDelta, currentFrameStartTime);
+ }
+
+ /**
+ * Abstract method to be overridden by subclasses.
+ * @param timeDelta the time between the beginning of last frame and the beginning of this frame, in ns
+ * @param currentFrameStartTime the startTime of the current frame, in ns.
+ * @return true if animation should be run at the next frame, false otherwise
+ */
+ protected abstract boolean internalRun(long timeDelta, long currentFrameStartTime);
+
+ public RenderTask(boolean aRunAfter) {
+ runAfter = aRunAfter;
+ }
+
+ /**
+ * Get the start time of this task.
+ * It is the start time of the first frame this task was run on.
+ * @return the start time in ns
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ /**
+ * Schedule a reset of the recorded start time next time {@link RenderTask#run(long, long)} is run.
+ * @see RenderTask#getStartTime()
+ */
+ public void resetStartTime() {
+ mResetStartTime = true;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/TextureGenerator.java
@@ -0,0 +1,77 @@
+/* -*- 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.gfx;
+
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLContext;
+
+public class TextureGenerator {
+ private static final String LOGTAG = "TextureGenerator";
+ private static final int POOL_SIZE = 5;
+
+ private static TextureGenerator sSharedInstance;
+
+ private final ArrayBlockingQueue<Integer> mTextureIds;
+ private EGLContext mContext;
+
+ private TextureGenerator() { mTextureIds = new ArrayBlockingQueue<Integer>(POOL_SIZE); }
+
+ public static TextureGenerator get() {
+ if (sSharedInstance == null)
+ sSharedInstance = new TextureGenerator();
+ return sSharedInstance;
+ }
+
+ public synchronized int take() {
+ try {
+ // Will block until one becomes available
+ return (int)mTextureIds.take();
+ } catch (InterruptedException e) {
+ return 0;
+ }
+ }
+
+ public synchronized void fill() {
+ EGL10 egl = (EGL10)EGLContext.getEGL();
+ EGLContext context = egl.eglGetCurrentContext();
+
+ if (mContext != null && mContext != context) {
+ mTextureIds.clear();
+ }
+
+ mContext = context;
+
+ int numNeeded = mTextureIds.remainingCapacity();
+ if (numNeeded == 0)
+ return;
+
+ // Clear existing GL errors
+ int error;
+ while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ Log.w(LOGTAG, String.format("Clearing GL error: %#x", error));
+ }
+
+ int[] textures = new int[numNeeded];
+ GLES20.glGenTextures(numNeeded, textures, 0);
+
+ error = GLES20.glGetError();
+ if (error != GLES20.GL_NO_ERROR) {
+ Log.e(LOGTAG, String.format("Failed to generate textures: %#x", error), new Exception());
+ return;
+ }
+
+ for (int i = 0; i < numNeeded; i++) {
+ mTextureIds.offer(textures[i]);
+ }
+ }
+}
+
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/TextureReaper.java
@@ -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.gfx;
+
+import android.opengl.GLES20;
+
+import java.util.ArrayList;
+
+/** Manages a list of dead tiles, so we don't leak resources. */
+public class TextureReaper {
+ private static TextureReaper sSharedInstance;
+ private final ArrayList<Integer> mDeadTextureIDs;
+
+ private TextureReaper() { mDeadTextureIDs = new ArrayList<Integer>(); }
+
+ public static TextureReaper get() {
+ if (sSharedInstance == null)
+ sSharedInstance = new TextureReaper();
+ return sSharedInstance;
+ }
+
+ public void add(int[] textureIDs) {
+ for (int textureID : textureIDs)
+ add(textureID);
+ }
+
+ public void add(int textureID) {
+ mDeadTextureIDs.add(textureID);
+ }
+
+ public void reap() {
+ int numTextures = mDeadTextureIDs.size();
+ // Adreno 200 will generate INVALID_VALUE if len == 0 is passed to glDeleteTextures,
+ // even though it's not supposed to.
+ if (numTextures == 0)
+ return;
+
+ int[] deadTextureIDs = new int[numTextures];
+ for (int i = 0; i < numTextures; i++) {
+ deadTextureIDs[i] = mDeadTextureIDs.get(i);
+ }
+ mDeadTextureIDs.clear();
+
+ GLES20.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0);
+ }
+}
+
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java
@@ -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.gfx;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+@WrapForJNI
+public class ViewTransform {
+ public float x;
+ public float y;
+ public float width;
+ public float height;
+ public float scale;
+ public float fixedLayerMarginLeft;
+ public float fixedLayerMarginTop;
+ public float fixedLayerMarginRight;
+ public float fixedLayerMarginBottom;
+
+ public ViewTransform(float inX, float inY, float inScale) {
+ x = inX;
+ y = inY;
+ scale = inScale;
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/VirtualLayer.java
@@ -0,0 +1,36 @@
+/* -*- 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.gfx;
+
+public class VirtualLayer extends Layer {
+ public VirtualLayer(IntSize size) {
+ super(size);
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ // No-op.
+ }
+
+ void setPositionAndResolution(int left, int top, int right, int bottom, float newResolution) {
+ // This is an optimized version of the following code:
+ // beginTransaction();
+ // try {
+ // setPosition(new Rect(left, top, right, bottom));
+ // setResolution(newResolution);
+ // performUpdates(null);
+ // } finally {
+ // endTransaction();
+ // }
+
+ // it is safe to drop the transaction lock in this instance (i.e. for the
+ // VirtualLayer that is just a shadow of what gecko is painting) because
+ // the position and resolution of this layer are always touched on the compositor
+ // thread, and therefore do not require synchronization.
+ mPosition.set(left, top, right, bottom);
+ mResolution = newResolution;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
@@ -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.mozglue;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+class ByteBufferInputStream extends InputStream {
+
+ protected ByteBuffer mBuf;
+ // Reference to a native object holding the data backing the ByteBuffer.
+ private final NativeReference mNativeRef;
+
+ protected ByteBufferInputStream(ByteBuffer buffer, NativeReference ref) {
+ mBuf = buffer;
+ mNativeRef = ref;
+ }
+
+ @Override
+ public int available() {
+ return mBuf.remaining();
+ }
+
+ @Override
+ public void close() {
+ mBuf = null;
+ mNativeRef.release();
+ }
+
+ @Override
+ public int read() {
+ if (!mBuf.hasRemaining() || mNativeRef.isReleased()) {
+ return -1;
+ }
+
+ return mBuf.get() & 0xff; // Avoid sign extension
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) {
+ if (!mBuf.hasRemaining() || mNativeRef.isReleased()) {
+ return -1;
+ }
+
+ length = Math.min(length, mBuf.remaining());
+ mBuf.get(buffer, offset, length);
+ return length;
+ }
+
+ @Override
+ public long skip(long byteCount) {
+ if (byteCount < 0 || mNativeRef.isReleased()) {
+ return 0;
+ }
+
+ byteCount = Math.min(byteCount, mBuf.remaining());
+ mBuf.position(mBuf.position() + (int)byteCount);
+ return byteCount;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
@@ -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.mozglue;
+
+import java.nio.ByteBuffer;
+
+//
+// We must manually allocate direct buffers in JNI to work around a bug where Honeycomb's
+// ByteBuffer.allocateDirect() grossly overallocates the direct buffer size.
+// https://code.google.com/p/android/issues/detail?id=16941
+//
+
+public final class DirectBufferAllocator {
+ private DirectBufferAllocator() {}
+
+ public static ByteBuffer allocate(int size) {
+ if (size <= 0) {
+ throw new IllegalArgumentException("Invalid size " + size);
+ }
+
+ ByteBuffer directBuffer = nativeAllocateDirectBuffer(size);
+ if (directBuffer == null) {
+ throw new OutOfMemoryError("allocateDirectBuffer() returned null");
+ }
+
+ if (!directBuffer.isDirect()) {
+ throw new AssertionError("allocateDirectBuffer() did not return a direct buffer");
+ }
+
+ return directBuffer;
+ }
+
+ public static ByteBuffer free(ByteBuffer buffer) {
+ if (buffer == null) {
+ return null;
+ }
+
+ if (!buffer.isDirect()) {
+ throw new IllegalArgumentException("buffer must be direct");
+ }
+
+ nativeFreeDirectBuffer(buffer);
+ return null;
+ }
+
+ // These JNI methods are implemented in mozglue/android/nsGeckoUtils.cpp.
+ private static native ByteBuffer nativeAllocateDirectBuffer(long size);
+ private static native void nativeFreeDirectBuffer(ByteBuffer buf);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
@@ -0,0 +1,548 @@
+/* -*- 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.mozglue;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.AppConstants;
+
+public final class GeckoLoader {
+ private static final String LOGTAG = "GeckoLoader";
+
+ private static volatile SafeIntent sIntent;
+ private static File sCacheFile;
+ private static File sGREDir;
+
+ /* Synchronized on GeckoLoader.class. */
+ private static boolean sSQLiteLibsLoaded;
+ private static boolean sNSSLibsLoaded;
+ private static boolean sMozGlueLoaded;
+
+ private GeckoLoader() {
+ // prevent instantiation
+ }
+
+ public static File getCacheDir(Context context) {
+ if (sCacheFile == null) {
+ sCacheFile = context.getCacheDir();
+ }
+ return sCacheFile;
+ }
+
+ public static File getGREDir(Context context) {
+ if (sGREDir == null) {
+ sGREDir = new File(context.getApplicationInfo().dataDir);
+ }
+ return sGREDir;
+ }
+
+ private static void setupPluginEnvironment(Context context, String[] pluginDirs) {
+ // setup plugin path directories
+ try {
+ // Check to see if plugins were blocked.
+ if (pluginDirs == null) {
+ putenv("MOZ_PLUGINS_BLOCKED=1");
+ putenv("MOZ_PLUGIN_PATH=");
+ return;
+ }
+
+ StringBuilder pluginSearchPath = new StringBuilder();
+ for (int i = 0; i < pluginDirs.length; i++) {
+ pluginSearchPath.append(pluginDirs[i]);
+ pluginSearchPath.append(":");
+ }
+ putenv("MOZ_PLUGIN_PATH=" + pluginSearchPath);
+
+ File pluginDataDir = context.getDir("plugins", 0);
+ putenv("ANDROID_PLUGIN_DATADIR=" + pluginDataDir.getPath());
+
+ File pluginPrivateDataDir = context.getDir("plugins_private", 0);
+ putenv("ANDROID_PLUGIN_DATADIR_PRIVATE=" + pluginPrivateDataDir.getPath());
+
+ } catch (Exception ex) {
+ Log.w(LOGTAG, "Caught exception getting plugin dirs.", ex);
+ }
+ }
+
+ private static void setupDownloadEnvironment(final Context context) {
+ try {
+ File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ File updatesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+ if (downloadDir == null) {
+ downloadDir = new File(Environment.getExternalStorageDirectory().getPath(), "download");
+ }
+ if (updatesDir == null) {
+ updatesDir = downloadDir;
+ }
+ putenv("DOWNLOADS_DIRECTORY=" + downloadDir.getPath());
+ putenv("UPDATES_DIRECTORY=" + updatesDir.getPath());
+ } catch (Exception e) {
+ Log.w(LOGTAG, "No download directory found.", e);
+ }
+ }
+
+ private static void delTree(File file) {
+ if (file.isDirectory()) {
+ File children[] = file.listFiles();
+ for (File child : children) {
+ delTree(child);
+ }
+ }
+ file.delete();
+ }
+
+ private static File getTmpDir(Context context) {
+ File tmpDir = context.getDir("tmpdir", Context.MODE_PRIVATE);
+ // check if the old tmp dir is there
+ File oldDir = new File(tmpDir.getParentFile(), "app_tmp");
+ if (oldDir.exists()) {
+ delTree(oldDir);
+ }
+ return tmpDir;
+ }
+
+ public static void setLastIntent(SafeIntent intent) {
+ sIntent = intent;
+ }
+
+ public static void setupGeckoEnvironment(Context context, String[] pluginDirs, String profilePath) {
+ // if we have an intent (we're being launched by an activity)
+ // read in any environmental variables from it here
+ final SafeIntent intent = sIntent;
+ if (intent != null) {
+ String env = intent.getStringExtra("env0");
+ Log.d(LOGTAG, "Gecko environment env0: " + env);
+ for (int c = 1; env != null; c++) {
+ putenv(env);
+ env = intent.getStringExtra("env" + c);
+ Log.d(LOGTAG, "env" + c + ": " + env);
+ }
+ }
+
+ putenv("MOZ_ANDROID_PACKAGE_NAME=" + context.getPackageName());
+
+ setupPluginEnvironment(context, pluginDirs);
+ setupDownloadEnvironment(context);
+
+ // profile home path
+ putenv("HOME=" + profilePath);
+
+ // setup the tmp path
+ File f = getTmpDir(context);
+ if (!f.exists()) {
+ f.mkdirs();
+ }
+ putenv("TMPDIR=" + f.getPath());
+
+ // setup the downloads path
+ f = Environment.getDownloadCacheDirectory();
+ putenv("EXTERNAL_STORAGE=" + f.getPath());
+
+ // setup the app-specific cache path
+ f = context.getCacheDir();
+ putenv("CACHE_DIRECTORY=" + f.getPath());
+
+ if (AppConstants.Versions.feature17Plus) {
+ android.os.UserManager um = (android.os.UserManager)context.getSystemService(Context.USER_SERVICE);
+ if (um != null) {
+ putenv("MOZ_ANDROID_USER_SERIAL_NUMBER=" + um.getSerialNumberForUser(android.os.Process.myUserHandle()));
+ } else {
+ Log.d(LOGTAG, "Unable to obtain user manager service on a device with SDK version " + Build.VERSION.SDK_INT);
+ }
+ }
+ setupLocaleEnvironment();
+
+ // We don't need this any more.
+ sIntent = null;
+ }
+
+ private static void loadLibsSetupLocked(Context context) {
+ // The package data lib directory isn't placed in ld.so's
+ // search path, so we have to manually load libraries that
+ // libxul will depend on. Not ideal.
+
+ File cacheFile = getCacheDir(context);
+ putenv("GRE_HOME=" + getGREDir(context).getPath());
+
+ // setup the libs cache
+ String linkerCache = System.getenv("MOZ_LINKER_CACHE");
+ if (linkerCache == null) {
+ linkerCache = cacheFile.getPath();
+ putenv("MOZ_LINKER_CACHE=" + linkerCache);
+ }
+
+ // Disable on-demand decompression of the linker on devices where it
+ // is known to cause crashes.
+ String forced_ondemand = System.getenv("MOZ_LINKER_ONDEMAND");
+ if (forced_ondemand == null) {
+ if ("HTC".equals(android.os.Build.MANUFACTURER) &&
+ "HTC Vision".equals(android.os.Build.MODEL)) {
+ putenv("MOZ_LINKER_ONDEMAND=0");
+ }
+ }
+
+ if (AppConstants.MOZ_LINKER_EXTRACT) {
+ putenv("MOZ_LINKER_EXTRACT=1");
+ // Ensure that the cache dir is world-writable
+ File cacheDir = new File(linkerCache);
+ if (cacheDir.isDirectory()) {
+ cacheDir.setWritable(true, false);
+ cacheDir.setExecutable(true, false);
+ cacheDir.setReadable(true, false);
+ }
+ }
+ }
+
+ @RobocopTarget
+ public synchronized static void loadSQLiteLibs(final Context context, final String apkName) {
+ if (sSQLiteLibsLoaded) {
+ return;
+ }
+
+ loadMozGlue(context);
+ loadLibsSetupLocked(context);
+ loadSQLiteLibsNative(apkName);
+ sSQLiteLibsLoaded = true;
+ }
+
+ public synchronized static void loadNSSLibs(final Context context, final String apkName) {
+ if (sNSSLibsLoaded) {
+ return;
+ }
+
+ loadMozGlue(context);
+ loadLibsSetupLocked(context);
+ loadNSSLibsNative(apkName);
+ sNSSLibsLoaded = true;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static final String getCPUABI() {
+ return android.os.Build.CPU_ABI;
+ }
+
+ /**
+ * Copy a library out of our APK.
+ *
+ * @param context a Context.
+ * @param lib the name of the library; e.g., "mozglue".
+ * @param outDir the output directory for the .so. No trailing slash.
+ * @return true on success, false on failure.
+ */
+ private static boolean extractLibrary(final Context context, final String lib, final String outDir) {
+ final String apkPath = context.getApplicationInfo().sourceDir;
+
+ // Sanity check.
+ if (!apkPath.endsWith(".apk")) {
+ Log.w(LOGTAG, "sourceDir is not an APK.");
+ return false;
+ }
+
+ // Try to extract the named library from the APK.
+ File outDirFile = new File(outDir);
+ if (!outDirFile.isDirectory()) {
+ if (!outDirFile.mkdirs()) {
+ Log.e(LOGTAG, "Couldn't create " + outDir);
+ return false;
+ }
+ }
+
+ if (AppConstants.Versions.feature21Plus) {
+ String[] abis = Build.SUPPORTED_ABIS;
+ for (String abi : abis) {
+ if (tryLoadWithABI(lib, outDir, apkPath, abi)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ final String abi = getCPUABI();
+ return tryLoadWithABI(lib, outDir, apkPath, abi);
+ }
+ }
+
+ private static boolean tryLoadWithABI(String lib, String outDir, String apkPath, String abi) {
+ try {
+ final ZipFile zipFile = new ZipFile(new File(apkPath));
+ try {
+ final String libPath = "lib/" + abi + "/lib" + lib + ".so";
+ final ZipEntry entry = zipFile.getEntry(libPath);
+ if (entry == null) {
+ Log.w(LOGTAG, libPath + " not found in APK " + apkPath);
+ return false;
+ }
+
+ final InputStream in = zipFile.getInputStream(entry);
+ try {
+ final String outPath = outDir + "/lib" + lib + ".so";
+ final FileOutputStream out = new FileOutputStream(outPath);
+ final byte[] bytes = new byte[1024];
+ int read;
+
+ Log.d(LOGTAG, "Copying " + libPath + " to " + outPath);
+ boolean failed = false;
+ try {
+ while ((read = in.read(bytes, 0, 1024)) != -1) {
+ out.write(bytes, 0, read);
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Failing library copy.", e);
+ failed = true;
+ } finally {
+ out.close();
+ }
+
+ if (failed) {
+ // Delete the partial copy so we don't fail to load it.
+ // Don't bother to check the return value -- there's nothing
+ // we can do about a failure.
+ new File(outPath).delete();
+ } else {
+ // Mark the file as executable. This doesn't seem to be
+ // necessary for the loader, but it's the normal state of
+ // affairs.
+ Log.d(LOGTAG, "Marking " + outPath + " as executable.");
+ new File(outPath).setExecutable(true);
+ }
+
+ return !failed;
+ } finally {
+ in.close();
+ }
+ } finally {
+ zipFile.close();
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failed to extract lib from APK.", e);
+ return false;
+ }
+ }
+
+ private static String getLoadDiagnostics(final Context context, final String lib) {
+ final String androidPackageName = context.getPackageName();
+
+ final StringBuilder message = new StringBuilder("LOAD ");
+ message.append(lib);
+
+ // These might differ. If so, we know why the library won't load!
+ message.append(": ABI: " + AppConstants.MOZ_APP_ABI + ", " + getCPUABI());
+ message.append(": Data: " + context.getApplicationInfo().dataDir);
+ try {
+ final boolean appLibExists = new File("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so").exists();
+ final boolean dataDataExists = new File("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so").exists();
+ message.append(", ax=" + appLibExists);
+ message.append(", ddx=" + dataDataExists);
+ } catch (Throwable e) {
+ message.append(": ax/ddx fail, ");
+ }
+
+ try {
+ final String dashOne = "/data/data/" + androidPackageName + "-1";
+ final String dashTwo = "/data/data/" + androidPackageName + "-2";
+ final boolean dashOneExists = new File(dashOne).exists();
+ final boolean dashTwoExists = new File(dashTwo).exists();
+ message.append(", -1x=" + dashOneExists);
+ message.append(", -2x=" + dashTwoExists);
+ } catch (Throwable e) {
+ message.append(", dash fail, ");
+ }
+
+ try {
+ if (Build.VERSION.SDK_INT >= 9) {
+ final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
+ final boolean nativeLibDirExists = new File(nativeLibPath).exists();
+ final boolean nativeLibLibExists = new File(nativeLibPath + "/lib" + lib + ".so").exists();
+
+ message.append(", nativeLib: " + nativeLibPath);
+ message.append(", dirx=" + nativeLibDirExists);
+ message.append(", libx=" + nativeLibLibExists);
+ } else {
+ message.append(", <pre-9>");
+ }
+ } catch (Throwable e) {
+ message.append(", nativeLib fail.");
+ }
+
+ return message.toString();
+ }
+
+ private static final boolean attemptLoad(final String path) {
+ try {
+ System.load(path);
+ return true;
+ } catch (Throwable e) {
+ Log.wtf(LOGTAG, "Couldn't load " + path + ": " + e);
+ }
+
+ return false;
+ }
+
+ /**
+ * The first two attempts at loading a library: directly, and
+ * then using the app library path.
+ *
+ * Returns null or the cause exception.
+ */
+ private static final Throwable doLoadLibraryExpected(final Context context, final String lib) {
+ try {
+ // Attempt 1: the way that should work.
+ System.loadLibrary(lib);
+ return null;
+ } catch (Throwable e) {
+ Log.wtf(LOGTAG, "Couldn't load " + lib + ". Trying native library dir.");
+
+ if (Build.VERSION.SDK_INT < 9) {
+ // We can't use nativeLibraryDir.
+ return e;
+ }
+
+ // Attempt 2: use nativeLibraryDir, which should also work.
+ final String libDir = context.getApplicationInfo().nativeLibraryDir;
+ final String libPath = libDir + "/lib" + lib + ".so";
+
+ // Does it even exist?
+ if (new File(libPath).exists()) {
+ if (attemptLoad(libPath)) {
+ // Success!
+ return null;
+ }
+ Log.wtf(LOGTAG, "Library exists but couldn't load!");
+ } else {
+ Log.wtf(LOGTAG, "Library doesn't exist when it should.");
+ }
+
+ // We failed. Return the original cause.
+ return e;
+ }
+ }
+
+ public static void doLoadLibrary(final Context context, final String lib) {
+ final Throwable e = doLoadLibraryExpected(context, lib);
+ if (e == null) {
+ // Success.
+ return;
+ }
+
+ // If we're in a mismatched UID state (Bug 1042935 Comment 16) there's really
+ // nothing we can do.
+ if (Build.VERSION.SDK_INT >= 9) {
+ final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
+ if (nativeLibPath.contains("mismatched_uid")) {
+ throw new RuntimeException("Fatal: mismatched UID: cannot load.");
+ }
+ }
+
+ // Attempt 3: try finding the path the pseudo-supported way using .dataDir.
+ final String dataLibPath = context.getApplicationInfo().dataDir + "/lib/lib" + lib + ".so";
+ if (attemptLoad(dataLibPath)) {
+ return;
+ }
+
+ // Attempt 4: use /data/app-lib directly. This is a last-ditch effort.
+ final String androidPackageName = context.getPackageName();
+ if (attemptLoad("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so")) {
+ return;
+ }
+
+ // Attempt 5: even more optimistic.
+ if (attemptLoad("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so")) {
+ return;
+ }
+
+ // Look in our files directory, copying from the APK first if necessary.
+ final String filesLibDir = context.getFilesDir() + "/lib";
+ final String filesLibPath = filesLibDir + "/lib" + lib + ".so";
+ if (new File(filesLibPath).exists()) {
+ if (attemptLoad(filesLibPath)) {
+ return;
+ }
+ } else {
+ // Try copying.
+ if (extractLibrary(context, lib, filesLibDir)) {
+ // Let's try it!
+ if (attemptLoad(filesLibPath)) {
+ return;
+ }
+ }
+ }
+
+ // Give up loudly, leaking information to debug the failure.
+ final String message = getLoadDiagnostics(context, lib);
+ Log.e(LOGTAG, "Load diagnostics: " + message);
+
+ // Throw the descriptive message, using the original library load
+ // failure as the cause.
+ throw new RuntimeException(message, e);
+ }
+
+ public synchronized static void loadMozGlue(final Context context) {
+ if (sMozGlueLoaded) {
+ return;
+ }
+
+ doLoadLibrary(context, "mozglue");
+ sMozGlueLoaded = true;
+ }
+
+ public synchronized static void loadGeckoLibs(final Context context, final String apkName) {
+ loadLibsSetupLocked(context);
+ loadGeckoLibsNative(apkName);
+ }
+
+ private static void setupLocaleEnvironment() {
+ putenv("LANG=" + Locale.getDefault().toString());
+ NumberFormat nf = NumberFormat.getInstance();
+ if (nf instanceof DecimalFormat) {
+ DecimalFormat df = (DecimalFormat)nf;
+ DecimalFormatSymbols dfs = df.getDecimalFormatSymbols();
+
+ putenv("LOCALE_DECIMAL_POINT=" + dfs.getDecimalSeparator());
+ putenv("LOCALE_THOUSANDS_SEP=" + dfs.getGroupingSeparator());
+ putenv("LOCALE_GROUPING=" + (char)df.getGroupingSize());
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class AbortException extends Exception {
+ public AbortException(String msg) {
+ super(msg);
+ }
+ }
+
+ @JNITarget
+ public static void abort(final String msg) {
+ final Thread thread = Thread.currentThread();
+ final Thread.UncaughtExceptionHandler uncaughtHandler =
+ thread.getUncaughtExceptionHandler();
+ if (uncaughtHandler != null) {
+ uncaughtHandler.uncaughtException(thread, new AbortException(msg));
+ }
+ }
+
+ // These methods are implemented in mozglue/android/nsGeckoUtils.cpp
+ private static native void putenv(String map);
+
+ // These methods are implemented in mozglue/android/APKOpen.cpp
+ public static native void nativeRun(String args);
+ private static native void loadGeckoLibsNative(String apkName);
+ private static native void loadSQLiteLibsNative(String apkName);
+ private static native void loadNSSLibsNative(String apkName);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java
@@ -0,0 +1,11 @@
+package org.mozilla.gecko.mozglue;
+
+// Class that all classes with native methods extend from.
+public abstract class JNIObject
+{
+ // Pointer to a WeakPtr object that refers to the native object.
+ private long mHandle;
+
+ // Dispose of any reference to a native object.
+ protected abstract void disposeNative();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java
@@ -0,0 +1,13 @@
+/* -*- 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.mozglue;
+
+public interface NativeReference
+{
+ public void release();
+
+ public boolean isReleased();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java
@@ -0,0 +1,86 @@
+/* -*- 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.mozglue;
+
+import org.mozilla.gecko.annotation.JNITarget;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+public class NativeZip implements NativeReference {
+ private static final int DEFLATE = 8;
+ private static final int STORE = 0;
+
+ private volatile long mObj;
+ private InputStream mInput;
+
+ public NativeZip(String path) {
+ mObj = getZip(path);
+ }
+
+ public NativeZip(InputStream input) {
+ if (!(input instanceof ByteBufferInputStream)) {
+ throw new IllegalArgumentException("Got " + input.getClass()
+ + ", but expected ByteBufferInputStream!");
+ }
+ ByteBufferInputStream bbinput = (ByteBufferInputStream)input;
+ mObj = getZipFromByteBuffer(bbinput.mBuf);
+ mInput = input;
+ }
+
+ @Override
+ public void finalize() {
+ release();
+ }
+
+ public void close() {
+ release();
+ }
+
+ @Override
+ public void release() {
+ if (mObj != 0) {
+ _release(mObj);
+ mObj = 0;
+ }
+ mInput = null;
+ }
+
+ @Override
+ public boolean isReleased() {
+ return (mObj == 0);
+ }
+
+ public InputStream getInputStream(String path) {
+ if (isReleased()) {
+ throw new IllegalStateException("Can't get path \"" + path
+ + "\" because NativeZip is closed!");
+ }
+ return _getInputStream(mObj, path);
+ }
+
+ private static native long getZip(String path);
+ private static native long getZipFromByteBuffer(ByteBuffer buffer);
+ private static native void _release(long obj);
+ private native InputStream _getInputStream(long obj, String path);
+
+ @JNITarget
+ private InputStream createInputStream(ByteBuffer buffer, int compression) {
+ if (compression != STORE && compression != DEFLATE) {
+ throw new IllegalArgumentException("Unexpected compression: " + compression);
+ }
+
+ InputStream input = new ByteBufferInputStream(buffer, this);
+ if (compression == DEFLATE) {
+ Inflater inflater = new Inflater(true);
+ input = new InflaterInputStream(input, inflater);
+ }
+
+ return input;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java
@@ -0,0 +1,96 @@
+/*
+ * 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/.
+ */
+
+// This should be in util/, but is here because of build dependency issues.
+package org.mozilla.gecko.mozglue;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * External applications can pass values into Intents that can cause us to crash: in defense,
+ * we wrap {@link Intent} and catch the exceptions they may force us to throw. See bug 1090385
+ * for more.
+ */
+public class SafeIntent {
+ private static final String LOGTAG = "Gecko" + SafeIntent.class.getSimpleName();
+
+ private final Intent intent;
+
+ public SafeIntent(final Intent intent) {
+ this.intent = intent;
+ }
+
+ public boolean getBooleanExtra(final String name, final boolean defaultValue) {
+ try {
+ return intent.getBooleanExtra(name, defaultValue);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return defaultValue;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return defaultValue;
+ }
+ }
+
+ public String getStringExtra(final String name) {
+ try {
+ return intent.getStringExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return null;
+ }
+ }
+
+ public Bundle getBundleExtra(final String name) {
+ try {
+ return intent.getBundleExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return null;
+ }
+ }
+
+ public String getAction() {
+ return intent.getAction();
+ }
+
+ public String getDataString() {
+ try {
+ return intent.getDataString();
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent data string: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent data string.", e);
+ return null;
+ }
+ }
+
+ public Uri getData() {
+ try {
+ return intent.getData();
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent data: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent data.", e);
+ return null;
+ }
+ }
+
+ public Intent getUnsafe() {
+ return intent;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java
@@ -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.permissions;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+/**
+ * Helper class to run code blocks depending on whether a user has granted or denied certain runtime permissions.
+ */
+public class PermissionBlock {
+ private final PermissionsHelper helper;
+
+ private Context context;
+ private String[] permissions;
+ private boolean onUIThread;
+ private Runnable onPermissionsGranted;
+ private Runnable onPermissionsDenied;
+ private boolean doNotPrompt;
+
+ /* package-private */ PermissionBlock(Context context, PermissionsHelper helper) {
+ this.context = context;
+ this.helper = helper;
+ }
+
+ /**
+ * Determine whether the app has been granted the specified permissions.
+ */
+ public PermissionBlock withPermissions(@NonNull String... permissions) {
+ this.permissions = permissions;
+ return this;
+ }
+
+ /**
+ * Execute all callbacks on the UI thread.
+ */
+ public PermissionBlock onUIThread() {
+ this.onUIThread = true;
+ return this;
+ }
+
+ /**
+ * Do not prompt the user to accept the permission if it has not been granted yet.
+ */
+ public PermissionBlock doNotPrompt() {
+ doNotPrompt = true;
+ return this;
+ }
+
+ /**
+ * If the condition is true then do not prompt the user to accept the permission if it has not
+ * been granted yet.
+ */
+ public PermissionBlock doNotPromptIf(boolean condition) {
+ if (condition) {
+ doNotPrompt();
+ }
+
+ return this;
+ }
+
+ /**
+ * Execute this permission block. Calling this method will prompt the user if needed.
+ */
+ public void run() {
+ run(null);
+ }
+
+ /**
+ * Execute the specified runnable if the app has been granted all permissions. Calling this method will prompt the
+ * user if needed.
+ */
+ public void run(Runnable onPermissionsGranted) {
+ if (!doNotPrompt && !(context instanceof Activity)) {
+ throw new IllegalStateException("You need to either specify doNotPrompt() or pass in an Activity context");
+ }
+
+ this.onPermissionsGranted = onPermissionsGranted;
+
+ if (hasPermissions(context)) {
+ onPermissionsGranted();
+ } else if (doNotPrompt) {
+ onPermissionsDenied();
+ } else {
+ Permissions.prompt((Activity) context, this);
+ }
+
+ // This reference is no longer needed. Let's clear it now to avoid memory leaks.
+ context = null;
+ }
+
+ /**
+ * Execute this fallback if at least one permission has not been granted.
+ */
+ public PermissionBlock andFallback(@NonNull Runnable onPermissionsDenied) {
+ this.onPermissionsDenied = onPermissionsDenied;
+ return this;
+ }
+
+ /* package-private */ void onPermissionsGranted() {
+ executeRunnable(onPermissionsGranted);
+ }
+
+ /* package-private */ void onPermissionsDenied() {
+ executeRunnable(onPermissionsDenied);
+ }
+
+ private void executeRunnable(Runnable runnable) {
+ if (runnable == null) {
+ return;
+ }
+
+ if (onUIThread) {
+ ThreadUtils.postToUiThread(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
+ /* package-private */ String[] getPermissions() {
+ return permissions;
+ }
+
+ /* packacge-private */ boolean hasPermissions(Context context) {
+ return helper.hasPermissions(context, permissions);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java
@@ -0,0 +1,210 @@
+/* -*- 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.permissions;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Convenience class for checking and prompting for runtime permissions.
+ *
+ * Example:
+ *
+ * Permissions.from(activity)
+ * .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ * .onUiThread()
+ * .andFallback(onPermissionDenied())
+ * .run(onPermissionGranted())
+ *
+ * This example will run the runnable returned by onPermissionGranted() if the WRITE_EXTERNAL_STORAGE permission is
+ * already granted. Otherwise it will prompt the user and run the runnable returned by onPermissionGranted() or
+ * onPermissionDenied() depending on whether the user accepted or not. If onUiThread() is specified then all callbacks
+ * will be run on the UI thread.
+ */
+public class Permissions {
+ private static final Queue<PermissionBlock> waiting = new LinkedList<>();
+ private static final Queue<PermissionBlock> prompt = new LinkedList<>();
+
+ private static PermissionsHelper permissionHelper = new PermissionsHelper();
+
+ /**
+ * Entry point for checking (and optionally prompting for) runtime permissions.
+ *
+ * Note: The provided context needs to be an Activity context in order to prompt. Use doNotPrompt()
+ * for all other contexts.
+ */
+ public static PermissionBlock from(@NonNull Context context) {
+ return new PermissionBlock(context, permissionHelper);
+ }
+
+ /**
+ * This method will block until the specified permissions have been granted or denied by the user.
+ * If needed the user will be prompted.
+ *
+ * @return true if all of the permissions have been granted. False if any of the permissions have been denied.
+ */
+ public static boolean waitFor(@NonNull Activity activity, String... permissions) {
+ ThreadUtils.assertNotOnUiThread(); // We do not want to block the UI thread.
+
+ // This task will block until all of the permissions have been granted
+ final FutureTask<Boolean> blockingTask = new FutureTask<>(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return true;
+ }
+ });
+
+ // This runnable will cancel the task if any of the permissions have been denied
+ Runnable cancelBlockingTask = new Runnable() {
+ @Override
+ public void run() {
+ blockingTask.cancel(true);
+ }
+ };
+
+ Permissions.from(activity)
+ .withPermissions(permissions)
+ .andFallback(cancelBlockingTask)
+ .run(blockingTask);
+
+ try {
+ return blockingTask.get();
+ } catch (InterruptedException | ExecutionException | CancellationException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Determine whether you have been granted particular permissions.
+ */
+ public static boolean has(Context context, String... permissions) {
+ return permissionHelper.hasPermissions(context, permissions);
+ }
+
+ /* package-private */ static void setPermissionHelper(PermissionsHelper permissionHelper) {
+ Permissions.permissionHelper = permissionHelper;
+ }
+
+ /**
+ * Callback for Activity.onRequestPermissionsResult(). All activities that prompt for permissions using this class
+ * should implement onRequestPermissionsResult() and call this method.
+ */
+ public static synchronized void onRequestPermissionsResult(@NonNull Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ processGrantResults(permissions, grantResults);
+
+ processQueue(activity, permissions, grantResults);
+ }
+
+ /* package-private */ static synchronized void prompt(Activity activity, PermissionBlock block) {
+ if (prompt.isEmpty()) {
+ prompt.add(block);
+ showPrompt(activity);
+ } else {
+ waiting.add(block);
+ }
+ }
+
+ private static synchronized void processGrantResults(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ final HashSet<String> grantedPermissions = collectGrantedPermissions(permissions, grantResults);
+
+ while (!prompt.isEmpty()) {
+ final PermissionBlock block = prompt.poll();
+
+ if (allPermissionsGranted(block, grantedPermissions)) {
+ block.onPermissionsGranted();
+ } else {
+ block.onPermissionsDenied();
+ }
+ }
+ }
+
+ private static synchronized void processQueue(Activity activity, String[] permissions, int[] grantResults) {
+ final HashSet<String> deniedPermissions = collectDeniedPermissions(permissions, grantResults);
+
+ while (!waiting.isEmpty()) {
+ final PermissionBlock block = waiting.poll();
+
+ if (block.hasPermissions(activity)) {
+ block.onPermissionsGranted();
+ } else {
+ if (atLeastOnePermissionDenied(block, deniedPermissions)) {
+ // We just prompted the user and one of the permissions of this block has been denied:
+ // There's no reason to instantly prompt again; Just reject without prompting.
+ block.onPermissionsDenied();
+ } else {
+ prompt.add(block);
+ }
+ }
+ }
+
+ if (!prompt.isEmpty()) {
+ showPrompt(activity);
+ }
+ }
+
+ private static synchronized void showPrompt(Activity activity) {
+ HashSet<String> permissions = new HashSet<>();
+
+ for (PermissionBlock block : prompt) {
+ Collections.addAll(permissions, block.getPermissions());
+ }
+
+ permissionHelper.prompt(activity, permissions.toArray(new String[permissions.size()]));
+ }
+
+ private static HashSet<String> collectGrantedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_GRANTED);
+ }
+
+ private static HashSet<String> collectDeniedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_DENIED);
+ }
+
+ private static HashSet<String> filterPermissionsByResult(@NonNull String[] permissions, @NonNull int[] grantResults, int result) {
+ HashSet<String> grantedPermissions = new HashSet<>(permissions.length);
+ for (int i = 0; i < permissions.length; i++) {
+ if (grantResults[i] == result) {
+ grantedPermissions.add(permissions[i]);
+ }
+ }
+ return grantedPermissions;
+ }
+
+ private static boolean allPermissionsGranted(PermissionBlock block, HashSet<String> grantedPermissions) {
+ for (String permission : block.getPermissions()) {
+ if (!grantedPermissions.contains(permission)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean atLeastOnePermissionDenied(PermissionBlock block, HashSet<String> deniedPermissions) {
+ for (String permission : block.getPermissions()) {
+ if (deniedPermissions.contains(permission)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java
@@ -0,0 +1,32 @@
+/* -*- 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.permissions;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+
+/* package-private */ class PermissionsHelper {
+ private static final int PERMISSIONS_REQUEST_CODE = 212;
+
+ public boolean hasPermissions(Context context, String... permissions) {
+ for (String permission : permissions) {
+ final int permissionCheck = ContextCompat.checkSelfPermission(context, permission);
+
+ if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void prompt(Activity activity, String[] permissions) {
+ ActivityCompat.requestPermissions(activity, permissions, PERMISSIONS_REQUEST_CODE);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
@@ -0,0 +1,34 @@
+/* -*- 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.restrictions;
+
+/**
+ * Default implementation of RestrictionConfiguration interface. Used whenever no restrictions are enforced for the
+ * current profile.
+ */
+public class DefaultConfiguration implements RestrictionConfiguration {
+ @Override
+ public boolean isAllowed(Restrictable restrictable) {
+ if (restrictable == Restrictable.BLOCK_LIST) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean canLoadUrl(String url) {
+ return true;
+ }
+
+ @Override
+ public boolean isRestricted() {
+ return false;
+ }
+
+ @Override
+ public void update() {}
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
@@ -0,0 +1,83 @@
+/* -*- 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.restrictions;
+
+import android.net.Uri;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * RestrictionConfiguration implementation for guest profiles.
+ */
+public class GuestProfileConfiguration implements RestrictionConfiguration {
+ static List<Restrictable> DISABLED_FEATURES = Arrays.asList(
+ Restrictable.DOWNLOAD,
+ Restrictable.INSTALL_EXTENSION,
+ Restrictable.INSTALL_APPS,
+ Restrictable.BROWSE,
+ Restrictable.SHARE,
+ Restrictable.BOOKMARK,
+ Restrictable.ADD_CONTACT,
+ Restrictable.SET_IMAGE,
+ Restrictable.MODIFY_ACCOUNTS,
+ Restrictable.REMOTE_DEBUGGING,
+ Restrictable.IMPORT_SETTINGS,
+ Restrictable.BLOCK_LIST,
+ Restrictable.DATA_CHOICES,
+ Restrictable.DEFAULT_THEME
+ );
+
+ @SuppressWarnings("serial")
+ private static final List<String> BANNED_SCHEMES = Arrays.asList(
+ "file",
+ "chrome",
+ "resource",
+ "jar",
+ "wyciwyg"
+ );
+
+ private static final List<String> BANNED_URLS = Arrays.asList(
+ "about:config",
+ "about:addons"
+ );
+
+ @Override
+ public boolean isAllowed(Restrictable restrictable) {
+ return !DISABLED_FEATURES.contains(restrictable);
+ }
+
+ @Override
+ public boolean canLoadUrl(String url) {
+ // Null URLs are always permitted.
+ if (url == null) {
+ return true;
+ }
+
+ final Uri u = Uri.parse(url);
+ final String scheme = u.getScheme();
+ if (BANNED_SCHEMES.contains(scheme)) {
+ return false;
+ }
+
+ url = url.toLowerCase();
+ for (String banned : BANNED_URLS) {
+ if (url.startsWith(banned)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean isRestricted() {
+ return true;
+ }
+
+ @Override
+ public void update() {}
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/Restrictable.java
@@ -0,0 +1,112 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.restrictions;
+
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.support.annotation.StringRes;
+
+/**
+ * This is a list of things we can restrict you from doing. Some of these are reflected in Android UserManager constants.
+ * Others are specific to us.
+ * These constants should be in sync with the ones from toolkit/components/parentalcontrols/nsIParentalControlsService.idl
+ */
+public enum Restrictable {
+ DOWNLOAD(1, "downloads", 0, 0),
+
+ INSTALL_EXTENSION(
+ 2, "no_install_extensions",
+ R.string.restrictable_feature_addons_installation,
+ R.string.restrictable_feature_addons_installation_description),
+
+ // UserManager.DISALLOW_INSTALL_APPS
+ INSTALL_APPS(3, "no_install_apps", 0 , 0),
+
+ BROWSE(4, "browse", 0, 0),
+
+ SHARE(5, "share", 0, 0),
+
+ BOOKMARK(6, "bookmark", 0, 0),
+
+ ADD_CONTACT(7, "add_contact", 0, 0),
+
+ SET_IMAGE(8, "set_image", 0, 0),
+
+ // UserManager.DISALLOW_MODIFY_ACCOUNTS
+ MODIFY_ACCOUNTS(9, "no_modify_accounts", 0, 0),
+
+ REMOTE_DEBUGGING(10, "remote_debugging", 0, 0),
+
+ IMPORT_SETTINGS(11, "import_settings", 0, 0),
+
+ PRIVATE_BROWSING(
+ 12, "private_browsing",
+ R.string.restrictable_feature_private_browsing,
+ R.string.restrictable_feature_private_browsing_description),
+
+ DATA_CHOICES(13, "data_coices", 0, 0),
+
+ CLEAR_HISTORY(14, "clear_history",
+ R.string.restrictable_feature_clear_history,
+ R.string.restrictable_feature_clear_history_description),
+
+ MASTER_PASSWORD(15, "master_password", 0, 0),
+
+ GUEST_BROWSING(16, "guest_browsing", 0, 0),
+
+ ADVANCED_SETTINGS(17, "advanced_settings",
+ R.string.restrictable_feature_advanced_settings,
+ R.string.restrictable_feature_advanced_settings_description),
+
+ CAMERA_MICROPHONE(18, "camera_microphone",
+ R.string.restrictable_feature_camera_microphone,
+ R.string.restrictable_feature_camera_microphone_description),
+
+ BLOCK_LIST(19, "block_list",
+ R.string.restrictable_feature_block_list,
+ R.string.restrictable_feature_block_list_description),
+
+ TELEMETRY(20, "telemetry",
+ R.string.datareporting_telemetry_title,
+ R.string.datareporting_telemetry_summary),
+
+ HEALTH_REPORT(21, "health_report",
+ R.string.datareporting_fhr_title,
+ R.string.datareporting_fhr_summary2),
+
+ DEFAULT_THEME(22, "default_theme", 0, 0);
+
+ public final int id;
+ public final String name;
+
+ @StringRes
+ public final int title;
+
+ @StringRes
+ public final int description;
+
+ Restrictable(final int id, final String name, @StringRes int title, @StringRes int description) {
+ this.id = id;
+ this.name = name;
+ this.title = title;
+ this.description = description;
+ }
+
+ public String getTitle(Context context) {
+ if (title == 0) {
+ return toString();
+ }
+ return context.getResources().getString(title);
+ }
+
+ public String getDescription(Context context) {
+ if (description == 0) {
+ return null;
+ }
+ return context.getResources().getString(description);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
@@ -0,0 +1,129 @@
+/* -*- 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.restrictions;
+
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.UserManager;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class RestrictedProfileConfiguration implements RestrictionConfiguration {
+ // Mapping from restrictable feature to default state (on/off)
+ private static Map<Restrictable, Boolean> configuration = new LinkedHashMap<>();
+ static {
+ configuration.put(Restrictable.INSTALL_EXTENSION, false);
+ configuration.put(Restrictable.PRIVATE_BROWSING, false);
+ configuration.put(Restrictable.CLEAR_HISTORY, false);
+ configuration.put(Restrictable.MASTER_PASSWORD, false);
+ configuration.put(Restrictable.GUEST_BROWSING, false);
+ configuration.put(Restrictable.ADVANCED_SETTINGS, false);
+ configuration.put(Restrictable.CAMERA_MICROPHONE, false);
+ configuration.put(Restrictable.DATA_CHOICES, false);
+ configuration.put(Restrictable.BLOCK_LIST, false);
+ configuration.put(Restrictable.TELEMETRY, false);
+ configuration.put(Restrictable.HEALTH_REPORT, true);
+ configuration.put(Restrictable.DEFAULT_THEME, true);
+ }
+
+ /**
+ * These restrictions are hidden from the admin configuration UI.
+ */
+ private static List<Restrictable> hiddenRestrictions = new ArrayList<>();
+ static {
+ hiddenRestrictions.add(Restrictable.MASTER_PASSWORD);
+ hiddenRestrictions.add(Restrictable.GUEST_BROWSING);
+ hiddenRestrictions.add(Restrictable.DATA_CHOICES);
+ hiddenRestrictions.add(Restrictable.DEFAULT_THEME);
+
+ // Hold behind Nightly flag until we have an actual block list deployed.
+ if (!AppConstants.NIGHTLY_BUILD) {
+ hiddenRestrictions.add(Restrictable.BLOCK_LIST);
+ }
+ }
+
+ /* package-private */ static boolean shouldHide(Restrictable restrictable) {
+ return hiddenRestrictions.contains(restrictable);
+ }
+
+ /* package-private */ static Map<Restrictable, Boolean> getConfiguration() {
+ return configuration;
+ }
+
+ private Context context;
+
+ public RestrictedProfileConfiguration(Context context) {
+ this.context = context.getApplicationContext();
+ }
+
+ @Override
+ public synchronized boolean isAllowed(Restrictable restrictable) {
+ // Special casing system/user restrictions
+ if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) {
+ return RestrictionCache.getUserRestriction(context, restrictable.name);
+ }
+
+ if (!RestrictionCache.hasApplicationRestriction(context, restrictable.name) && !configuration.containsKey(restrictable)) {
+ // Always allow features that are not in the configuration
+ return true;
+ }
+
+ return RestrictionCache.getApplicationRestriction(context, restrictable.name, configuration.get(restrictable));
+ }
+
+ @Override
+ public boolean canLoadUrl(String url) {
+ if (!isAllowed(Restrictable.INSTALL_EXTENSION) && AboutPages.isAboutAddons(url)) {
+ return false;
+ }
+
+ if (!isAllowed(Restrictable.PRIVATE_BROWSING) && AboutPages.isAboutPrivateBrowsing(url)) {
+ return false;
+ }
+
+ if (AboutPages.isAboutConfig(url)) {
+ // Always block access to about:config to prevent circumventing restrictions (Bug 1189233)
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean isRestricted() {
+ return true;
+ }
+
+ @Override
+ public synchronized void update() {
+ RestrictionCache.invalidate();
+ }
+
+ public static List<Restrictable> getVisibleRestrictions() {
+ final List<Restrictable> visibleList = new ArrayList<>();
+
+ for (Restrictable restrictable : configuration.keySet()) {
+ if (hiddenRestrictions.contains(restrictable)) {
+ continue;
+ }
+ visibleList.add(restrictable);
+ }
+
+ return visibleList;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionCache.java
@@ -0,0 +1,99 @@
+/* -*- 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.restrictions;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.UserManager;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * Cache for user and application restrictions.
+ */
+public class RestrictionCache {
+ private static Bundle cachedAppRestrictions;
+ private static Bundle cachedUserRestrictions;
+ private static boolean isCacheInvalid = true;
+
+ private RestrictionCache() {}
+
+ public static synchronized boolean getUserRestriction(Context context, String restriction) {
+ updateCacheIfNeeded(context);
+ return cachedUserRestrictions.getBoolean(restriction);
+ }
+
+ public static synchronized boolean hasApplicationRestriction(Context context, String restriction) {
+ updateCacheIfNeeded(context);
+ return cachedAppRestrictions.containsKey(restriction);
+ }
+
+ public static synchronized boolean getApplicationRestriction(Context context, String restriction, boolean defaultValue) {
+ updateCacheIfNeeded(context);
+ return cachedAppRestrictions.getBoolean(restriction, defaultValue);
+ }
+
+ public static synchronized boolean hasApplicationRestrictions(Context context) {
+ updateCacheIfNeeded(context);
+ return !cachedAppRestrictions.isEmpty();
+ }
+
+ public static synchronized void invalidate() {
+ isCacheInvalid = true;
+ }
+
+ private static void updateCacheIfNeeded(Context context) {
+ // If we are not on the UI thread then we can just go ahead and read the values (Bug 1189347).
+ // Otherwise we read from the cache to avoid blocking the UI thread. If the cache is invalid
+ // then we hazard the consequences and just do the read.
+ if (isCacheInvalid || !ThreadUtils.isOnUiThread()) {
+ readRestrictions(context);
+ isCacheInvalid = false;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ private static void readRestrictions(Context context) {
+ final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE);
+
+ // If we do not have anything in the cache yet then this read might happen on the UI thread (Bug 1189347).
+ final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+
+ try {
+ Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName());
+ migrateRestrictionsIfNeeded(appRestrictions);
+
+ cachedAppRestrictions = appRestrictions;
+ cachedUserRestrictions = mgr.getUserRestrictions(); // Always implies disk read
+ } finally {
+ StrictMode.setThreadPolicy(policy);
+ }
+ }
+
+ /**
+ * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336).
+ */
+ /* package-private */ static void migrateRestrictionsIfNeeded(Bundle bundle) {
+ if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) {
+ bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions"));
+ }
+
+ if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) {
+ bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing"));
+ }
+
+ if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) {
+ bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history"));
+ }
+
+ if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) {
+ bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings"));
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
@@ -0,0 +1,31 @@
+/* -*- 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.restrictions;
+
+/**
+ * Interface for classes that Restrictions will delegate to for making decisions.
+ */
+public interface RestrictionConfiguration {
+ /**
+ * Is the user allowed to perform this action?
+ */
+ boolean isAllowed(Restrictable restrictable);
+
+ /**
+ * Is the user allowed to load the given URL?
+ */
+ boolean canLoadUrl(String url);
+
+ /**
+ * Is this user restricted in any way?
+ */
+ boolean isRestricted();
+
+ /**
+ * Update restrictions if needed.
+ */
+ void update();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
@@ -0,0 +1,84 @@
+/* -*- 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.restrictions;
+
+import org.mozilla.gecko.AppConstants;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.RestrictionEntry;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * Broadcast receiver providing supported restrictions to the system.
+ */
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class RestrictionProvider extends BroadcastReceiver {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ if (AppConstants.Versions.preJBMR2) {
+ // This broadcast does not make any sense prior to Jelly Bean MR2.
+ return;
+ }
+
+ final PendingResult result = goAsync();
+
+ new Thread() {
+ @Override
+ public void run() {
+ final Bundle oldRestrictions = intent.getBundleExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE);
+ RestrictionCache.migrateRestrictionsIfNeeded(oldRestrictions);
+
+ final Bundle extras = new Bundle();
+
+ ArrayList<RestrictionEntry> entries = initRestrictions(context, oldRestrictions);
+ extras.putParcelableArrayList(Intent.EXTRA_RESTRICTIONS_LIST, entries);
+
+ result.setResult(Activity.RESULT_OK, null, extras);
+ result.finish();
+ }
+ }.start();
+ }
+
+ private ArrayList<RestrictionEntry> initRestrictions(Context context, Bundle oldRestrictions) {
+ ArrayList<RestrictionEntry> entries = new ArrayList<RestrictionEntry>();
+
+ final Map<Restrictable, Boolean> configuration = RestrictedProfileConfiguration.getConfiguration();
+
+ for (Restrictable restrictable : configuration.keySet()) {
+ if (RestrictedProfileConfiguration.shouldHide(restrictable)) {
+ continue;
+ }
+
+ RestrictionEntry entry = createRestrictionEntryWithDefaultValue(context, restrictable,
+ oldRestrictions.getBoolean(restrictable.name, configuration.get(restrictable)));
+ entries.add(entry);
+ }
+
+ return entries;
+ }
+
+ private RestrictionEntry createRestrictionEntryWithDefaultValue(Context context, Restrictable restrictable, boolean defaultValue) {
+ RestrictionEntry entry = new RestrictionEntry(restrictable.name, defaultValue);
+
+ entry.setTitle(restrictable.getTitle(context));
+
+ final String description = restrictable.getDescription(context);
+ if (!TextUtils.isEmpty(description)) {
+ entry.setDescription(description);
+ }
+
+ return entry;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/restrictions/Restrictions.java
@@ -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.restrictions;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.restrictions.DefaultConfiguration;
+import org.mozilla.gecko.restrictions.GuestProfileConfiguration;
+import org.mozilla.gecko.restrictions.Restrictable;
+import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
+import org.mozilla.gecko.restrictions.RestrictionCache;
+import org.mozilla.gecko.restrictions.RestrictionConfiguration;
+
+@RobocopTarget
+public class Restrictions {
+ private static final String LOGTAG = "GeckoRestrictedProfiles";
+
+ private static RestrictionConfiguration configuration;
+
+ private static RestrictionConfiguration getConfiguration(Context context) {
+ if (configuration == null) {
+ configuration = createConfiguration(context);
+ }
+
+ return configuration;
+ }
+
+ public static synchronized RestrictionConfiguration createConfiguration(Context context) {
+ if (configuration != null) {
+ // This method is synchronized and another thread might already have created the configuration.
+ return configuration;
+ }
+
+ if (isGuestProfile(context)) {
+ return new GuestProfileConfiguration();
+ } else if (isRestrictedProfile(context)) {
+ return new RestrictedProfileConfiguration(context);
+ } else {
+ return new DefaultConfiguration();
+ }
+ }
+
+ private static boolean isGuestProfile(Context context) {
+ if (configuration != null) {
+ return configuration instanceof GuestProfileConfiguration;
+ }
+
+ GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
+ if (geckoInterface != null) {
+ return geckoInterface.getProfile().inGuestMode();
+ }
+
+ return GeckoProfile.get(context).inGuestMode();
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ public static boolean isRestrictedProfile(Context context) {
+ if (configuration != null) {
+ return configuration instanceof RestrictedProfileConfiguration;
+ }
+
+ if (Versions.preJBMR2) {
+ // Early versions don't support restrictions at all
+ return false;
+ }
+
+ // The user is on a restricted profile if, and only if, we injected application restrictions during account setup.
+ return RestrictionCache.hasApplicationRestrictions(context);
+ }
+
+ public static void update(Context context) {
+ getConfiguration(context).update();
+ }
+
+ private static Restrictable geckoActionToRestriction(int action) {
+ for (Restrictable rest : Restrictable.values()) {
+ if (rest.id == action) {
+ return rest;
+ }
+ }
+
+ throw new IllegalArgumentException("Unknown action " + action);
+ }
+
+ private static boolean canLoadUrl(final Context context, final String url) {
+ return getConfiguration(context).canLoadUrl(url);
+ }
+
+ @WrapForJNI
+ public static boolean isUserRestricted() {
+ return isUserRestricted(GeckoAppShell.getApplicationContext());
+ }
+
+ public static boolean isUserRestricted(final Context context) {
+ return getConfiguration(context).isRestricted();
+ }
+
+ public static boolean isAllowed(final Context context, final Restrictable restrictable) {
+ return getConfiguration(context).isAllowed(restrictable);
+ }
+
+ @WrapForJNI
+ public static boolean isAllowed(int action, String url) {
+ final Restrictable restrictable;
+ try {
+ restrictable = geckoActionToRestriction(action);
+ } catch (IllegalArgumentException ex) {
+ // Unknown actions represent a coding error, so we
+ // refuse the action and log.
+ Log.e(LOGTAG, "Unknown action " + action + "; check calling code.");
+ return false;
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+
+ if (Restrictable.BROWSE == restrictable) {
+ return canLoadUrl(context, url);
+ } else {
+ return isAllowed(context, restrictable);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
@@ -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.sqlite;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/*
+ * Helper class to make the ByteBuffers returned by SQLite BLOB
+ * easier to use.
+ */
+public class ByteBufferInputStream extends InputStream {
+ private final ByteBuffer mByteBuffer;
+
+ public ByteBufferInputStream(ByteBuffer aByteBuffer) {
+ mByteBuffer = aByteBuffer;
+ }
+
+ @Override
+ public synchronized int read() throws IOException {
+ if (!mByteBuffer.hasRemaining()) {
+ return -1;
+ }
+ return mByteBuffer.get();
+ }
+
+ @Override
+ public synchronized int read(byte[] aBytes, int aOffset, int aLen)
+ throws IOException {
+ int toRead = Math.min(aLen, mByteBuffer.remaining());
+ mByteBuffer.get(aBytes, aOffset, toRead);
+ return toRead;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
@@ -0,0 +1,366 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+/*
+ * 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.sqlite;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants;
+
+import android.database.AbstractCursor;
+import android.database.CursorIndexOutOfBoundsException;
+import android.util.Log;
+
+/**
+ * A mutable cursor implementation backed by an array of {@code Object}s. Use
+ * {@link #newRow()} to add rows. Automatically expands internal capacity
+ * as needed.
+ *
+ * This class provides one missing feature from Android's MatrixCursor:
+ * the implementation of getBlob that was inadvertently omitted from API 9 (and
+ * perhaps later; it's present in 14).
+ *
+ * MatrixCursor is all private, so we entirely duplicate it here.
+ */
+public class MatrixBlobCursor extends AbstractCursor {
+ private static final String LOGTAG = "GeckoMatrixCursor";
+
+ private final String[] columnNames;
+ private final int columnCount;
+
+ private int rowCount;
+ private Throwable allocationStack;
+
+ Object[] data;
+
+ /**
+ * Constructs a new cursor with the given initial capacity.
+ *
+ * @param columnNames names of the columns, the ordering of which
+ * determines column ordering elsewhere in this cursor
+ * @param initialCapacity in rows
+ */
+ @WrapForJNI
+ public MatrixBlobCursor(String[] columnNames, int initialCapacity) {
+ this.columnNames = columnNames;
+ this.columnCount = columnNames.length;
+
+ if (initialCapacity < 1) {
+ initialCapacity = 1;
+ }
+
+ this.data = new Object[columnCount * initialCapacity];
+ if (AppConstants.DEBUG_BUILD) {
+ this.allocationStack = new Throwable("allocationStack");
+ }
+ }
+
+ /**
+ * Constructs a new cursor.
+ *
+ * @param columnNames names of the columns, the ordering of which
+ * determines column ordering elsewhere in this cursor
+ */
+ @WrapForJNI
+ public MatrixBlobCursor(String[] columnNames) {
+ this(columnNames, 16);
+ }
+
+ /**
+ * Closes the Cursor, releasing all of its resources.
+ */
+ public void close() {
+ this.allocationStack = null;
+ this.data = null;
+ super.close();
+ }
+
+ /**
+ * Gets value at the given column for the current row.
+ */
+ protected Object get(int column) {
+ if (column < 0 || column >= columnCount) {
+ throw new CursorIndexOutOfBoundsException("Requested column: "
+ + column + ", # of columns: " + columnCount);
+ }
+ if (mPos < 0) {
+ throw new CursorIndexOutOfBoundsException("Before first row.");
+ }
+ if (mPos >= rowCount) {
+ throw new CursorIndexOutOfBoundsException("After last row.");
+ }
+ return data[mPos * columnCount + column];
+ }
+
+ /**
+ * Adds a new row to the end and returns a builder for that row. Not safe
+ * for concurrent use.
+ *
+ * @return builder which can be used to set the column values for the new
+ * row
+ */
+ public RowBuilder newRow() {
+ rowCount++;
+ int endIndex = rowCount * columnCount;
+ ensureCapacity(endIndex);
+ int start = endIndex - columnCount;
+ return new RowBuilder(start, endIndex);
+ }
+
+ /**
+ * Adds a new row to the end with the given column values. Not safe
+ * for concurrent use.
+ *
+ * @throws IllegalArgumentException if {@code columnValues.length !=
+ * columnNames.length}
+ * @param columnValues in the same order as the the column names specified
+ * at cursor construction time
+ */
+ @WrapForJNI
+ public void addRow(Object[] columnValues) {
+ if (columnValues.length != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.length = "
+ + columnValues.length);
+ }
+
+ int start = rowCount++ * columnCount;
+ ensureCapacity(start + columnCount);
+ System.arraycopy(columnValues, 0, data, start, columnCount);
+ }
+
+ /**
+ * Adds a new row to the end with the given column values. Not safe
+ * for concurrent use.
+ *
+ * @throws IllegalArgumentException if {@code columnValues.size() !=
+ * columnNames.length}
+ * @param columnValues in the same order as the the column names specified
+ * at cursor construction time
+ */
+ @WrapForJNI
+ public void addRow(Iterable<?> columnValues) {
+ final int start = rowCount * columnCount;
+
+ if (columnValues instanceof ArrayList<?>) {
+ addRow((ArrayList<?>) columnValues, start);
+ return;
+ }
+
+ final int end = start + columnCount;
+ int current = start;
+
+ ensureCapacity(end);
+ final Object[] localData = data;
+ for (Object columnValue : columnValues) {
+ if (current == end) {
+ // TODO: null out row?
+ throw new IllegalArgumentException(
+ "columnValues.size() > columnNames.length");
+ }
+ localData[current++] = columnValue;
+ }
+
+ if (current != end) {
+ // TODO: null out row?
+ throw new IllegalArgumentException(
+ "columnValues.size() < columnNames.length");
+ }
+
+ // Increase row count here in case we encounter an exception.
+ rowCount++;
+ }
+
+ /** Optimization for {@link ArrayList}. */
+ @WrapForJNI
+ private void addRow(ArrayList<?> columnValues, int start) {
+ final int size = columnValues.size();
+ if (size != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.size() = " + size);
+ }
+
+ final int end = start + columnCount;
+ ensureCapacity(end);
+
+ // Take a reference just in case someone calls ensureCapacity
+ // and `data` gets replaced by a new array!
+ final Object[] localData = data;
+ for (int i = 0; i < size; i++) {
+ localData[start + i] = columnValues.get(i);
+ }
+
+ rowCount++;
+ }
+
+ /**
+ * Ensures that this cursor has enough capacity. If it needs to allocate
+ * a new array, the existing capacity will be at least doubled.
+ */
+ private void ensureCapacity(final int size) {
+ if (size <= data.length) {
+ return;
+ }
+
+ final Object[] oldData = this.data;
+ this.data = new Object[Math.max(size, data.length * 2)];
+ System.arraycopy(oldData, 0, this.data, 0, oldData.length);
+ }
+
+ /**
+ * Builds a row, starting from the left-most column and adding one column
+ * value at a time. Follows the same ordering as the column names specified
+ * at cursor construction time.
+ *
+ * Not thread-safe.
+ */
+ public class RowBuilder {
+ private int index;
+ private final int endIndex;
+
+ RowBuilder(int index, int endIndex) {
+ this.index = index;
+ this.endIndex = endIndex;
+ }
+
+ /**
+ * Sets the next column value in this row.
+ *
+ * @throws CursorIndexOutOfBoundsException if you try to add too many
+ * values
+ * @return this builder to support chaining
+ */
+ public RowBuilder add(final Object columnValue) {
+ if (index == endIndex) {
+ throw new CursorIndexOutOfBoundsException("No more columns left.");
+ }
+
+ data[index++] = columnValue;
+ return this;
+ }
+ }
+
+ /**
+ * Not thread safe.
+ */
+ public void set(int column, Object value) {
+ if (column < 0 || column >= columnCount) {
+ throw new CursorIndexOutOfBoundsException("Requested column: "
+ + column + ", # of columns: " + columnCount);
+ }
+ if (mPos < 0) {
+ throw new CursorIndexOutOfBoundsException("Before first row.");
+ }
+ if (mPos >= rowCount) {
+ throw new CursorIndexOutOfBoundsException("After last row.");
+ }
+ data[mPos * columnCount + column] = value;
+ }
+
+ // AbstractCursor implementation.
+ @Override
+ public int getCount() {
+ return rowCount;
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return columnNames;
+ }
+
+ @Override
+ public String getString(int column) {
+ Object value = get(column);
+ if (value == null) return null;
+ return value.toString();
+ }
+
+ @Override
+ public short getShort(int column) {
+ final Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).shortValue();
+ return Short.parseShort(value.toString());
+ }
+
+ @Override
+ public int getInt(int column) {
+ Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).intValue();
+ return Integer.parseInt(value.toString());
+ }
+
+ @Override
+ public long getLong(int column) {
+ Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).longValue();
+ return Long.parseLong(value.toString());
+ }
+
+ @Override
+ public float getFloat(int column) {
+ Object value = get(column);
+ if (value == null) return 0.0f;
+ if (value instanceof Number) return ((Number) value).floatValue();
+ return Float.parseFloat(value.toString());
+ }
+
+ @Override
+ public double getDouble(int column) {
+ Object value = get(column);
+ if (value == null) return 0.0d;
+ if (value instanceof Number) return ((Number) value).doubleValue();
+ return Double.parseDouble(value.toString());
+ }
+
+ @Override
+ public byte[] getBlob(int column) {
+ Object value = get(column);
+ if (value == null) return null;
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ }
+
+ if (value instanceof ByteBuffer) {
+ final ByteBuffer bytes = (ByteBuffer) value;
+ byte[] byteArray = new byte[bytes.remaining()];
+ bytes.get(byteArray);
+ return byteArray;
+ }
+ throw new UnsupportedOperationException("BLOB Object not of known type");
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return get(column) == null;
+ }
+
+ @Override
+ protected void finalize() {
+ if (AppConstants.DEBUG_BUILD) {
+ if (!isClosed()) {
+ Log.e(LOGTAG, "Cursor finalized without being closed", this.allocationStack);
+ }
+ }
+
+ super.finalize();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
@@ -0,0 +1,387 @@
+/* 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.sqlite;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map.Entry;
+
+/*
+ * This class allows using the mozsqlite3 library included with Firefox
+ * to read SQLite databases, instead of the Android SQLiteDataBase API,
+ * which might use whatever outdated DB is present on the Android system.
+ */
+public class SQLiteBridge {
+ private static final String LOGTAG = "SQLiteBridge";
+
+ // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
+ private final String mDb;
+
+ // Pointer to the database if it was opened with openDatabase. 0 implies closed.
+ protected volatile long mDbPointer;
+
+ // Values remembered after a query.
+ private long[] mQueryResults;
+
+ private boolean mTransactionSuccess;
+ private boolean mInTransaction;
+
+ private static final int RESULT_INSERT_ROW_ID = 0;
+ private static final int RESULT_ROWS_CHANGED = 1;
+
+ // Shamelessly cribbed from db/sqlite3/src/moz.build.
+ private static final int DEFAULT_PAGE_SIZE_BYTES = 32768;
+
+ // The same size we use elsewhere.
+ private static final int MAX_WAL_SIZE_BYTES = 524288;
+
+ // JNI code in $(topdir)/mozglue/android/..
+ private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery,
+ String[] aParams,
+ long[] aUpdateResult)
+ throws SQLiteBridgeException;
+ private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery,
+ String[] aParams,
+ long[] aUpdateResult)
+ throws SQLiteBridgeException;
+ private static native long openDatabase(String aDb)
+ throws SQLiteBridgeException;
+ private static native void closeDatabase(long aDb);
+
+ // Takes the path to the database we want to access.
+ @RobocopTarget
+ public SQLiteBridge(String aDb) throws SQLiteBridgeException {
+ mDb = aDb;
+ }
+
+ // Executes a simple line of sql.
+ public void execSQL(String sql)
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery(sql, null);
+ cursor.close();
+ }
+
+ // Executes a simple line of sql. Allow you to bind arguments
+ public void execSQL(String sql, String[] bindArgs)
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery(sql, bindArgs);
+ cursor.close();
+ }
+
+ // Executes a DELETE statement on the database
+ public int delete(String table, String whereClause, String[] whereArgs)
+ throws SQLiteBridgeException {
+ StringBuilder sb = new StringBuilder("DELETE from ");
+ sb.append(table);
+ if (whereClause != null) {
+ sb.append(" WHERE " + whereClause);
+ }
+
+ execSQL(sb.toString(), whereArgs);
+ return (int)mQueryResults[RESULT_ROWS_CHANGED];
+ }
+
+ public Cursor query(String table,
+ String[] columns,
+ String selection,
+ String[] selectionArgs,
+ String groupBy,
+ String having,
+ String orderBy,
+ String limit)
+ throws SQLiteBridgeException {
+ StringBuilder sb = new StringBuilder("SELECT ");
+ if (columns != null)
+ sb.append(TextUtils.join(", ", columns));
+ else
+ sb.append(" * ");
+
+ sb.append(" FROM ");
+ sb.append(table);
+
+ if (selection != null) {
+ sb.append(" WHERE " + selection);
+ }
+
+ if (groupBy != null) {
+ sb.append(" GROUP BY " + groupBy);
+ }
+
+ if (having != null) {
+ sb.append(" HAVING " + having);
+ }
+
+ if (orderBy != null) {
+ sb.append(" ORDER BY " + orderBy);
+ }
+
+ if (limit != null) {
+ sb.append(" " + limit);
+ }
+
+ return rawQuery(sb.toString(), selectionArgs);
+ }
+
+ @RobocopTarget
+ public Cursor rawQuery(String sql, String[] selectionArgs)
+ throws SQLiteBridgeException {
+ return internalQuery(sql, selectionArgs);
+ }
+
+ public long insert(String table, String nullColumnHack, ContentValues values)
+ throws SQLiteBridgeException {
+ if (values == null)
+ return 0;
+
+ ArrayList<String> valueNames = new ArrayList<String>();
+ ArrayList<String> valueBinds = new ArrayList<String>();
+ ArrayList<String> keyNames = new ArrayList<String>();
+
+ for (Entry<String, Object> value : values.valueSet()) {
+ keyNames.add(value.getKey());
+
+ Object val = value.getValue();
+ if (val == null) {
+ valueNames.add("NULL");
+ } else {
+ valueNames.add("?");
+ valueBinds.add(val.toString());
+ }
+ }
+
+ StringBuilder sb = new StringBuilder("INSERT into ");
+ sb.append(table);
+
+ sb.append(" (");
+ sb.append(TextUtils.join(", ", keyNames));
+ sb.append(")");
+
+ // XXX - Do we need to bind these values?
+ sb.append(" VALUES (");
+ sb.append(TextUtils.join(", ", valueNames));
+ sb.append(") ");
+
+ String[] binds = new String[valueBinds.size()];
+ valueBinds.toArray(binds);
+ execSQL(sb.toString(), binds);
+ return mQueryResults[RESULT_INSERT_ROW_ID];
+ }
+
+ public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
+ throws SQLiteBridgeException {
+ if (values == null)
+ return 0;
+
+ ArrayList<String> valueNames = new ArrayList<String>();
+
+ StringBuilder sb = new StringBuilder("UPDATE ");
+ sb.append(table);
+ sb.append(" SET ");
+
+ boolean isFirst = true;
+
+ for (Entry<String, Object> value : values.valueSet()) {
+ if (isFirst)
+ isFirst = false;
+ else
+ sb.append(", ");
+
+ sb.append(value.getKey());
+
+ Object val = value.getValue();
+ if (val == null) {
+ sb.append(" = NULL");
+ } else {
+ sb.append(" = ?");
+ valueNames.add(val.toString());
+ }
+ }
+
+ if (!TextUtils.isEmpty(whereClause)) {
+ sb.append(" WHERE ");
+ sb.append(whereClause);
+ valueNames.addAll(Arrays.asList(whereArgs));
+ }
+
+ String[] binds = new String[valueNames.size()];
+ valueNames.toArray(binds);
+
+ execSQL(sb.toString(), binds);
+ return (int)mQueryResults[RESULT_ROWS_CHANGED];
+ }
+
+ public int getVersion()
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery("PRAGMA user_version", null);
+ int ret = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ String version = cursor.getString(0);
+ ret = Integer.parseInt(version);
+ cursor.close();
+ }
+ return ret;
+ }
+
+ // Do an SQL query, substituting the parameters in the query with the passed
+ // parameters. The parameters are substituted in order: named parameters
+ // are not supported.
+ private Cursor internalQuery(String aQuery, String[] aParams)
+ throws SQLiteBridgeException {
+
+ mQueryResults = new long[2];
+ if (isOpen()) {
+ return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults);
+ }
+ return sqliteCall(mDb, aQuery, aParams, mQueryResults);
+ }
+
+ /*
+ * The second two parameters here are just provided for compatibility with SQLiteDatabase
+ * Support for them is not currently implemented.
+ */
+ public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
+ throws SQLiteException {
+ if (factory != null) {
+ throw new RuntimeException("factory not supported.");
+ }
+ if (flags != 0) {
+ throw new RuntimeException("flags not supported.");
+ }
+
+ SQLiteBridge bridge = null;
+ try {
+ bridge = new SQLiteBridge(path);
+ bridge.mDbPointer = SQLiteBridge.openDatabase(path);
+ } catch (SQLiteBridgeException ex) {
+ // Catch and rethrow as a SQLiteException to match SQLiteDatabase.
+ throw new SQLiteException(ex.getMessage());
+ }
+
+ prepareWAL(bridge);
+
+ return bridge;
+ }
+
+ public void close() {
+ if (isOpen()) {
+ closeDatabase(mDbPointer);
+ }
+ mDbPointer = 0L;
+ }
+
+ public boolean isOpen() {
+ return mDbPointer != 0;
+ }
+
+ public void beginTransaction() throws SQLiteBridgeException {
+ if (inTransaction()) {
+ throw new SQLiteBridgeException("Nested transactions are not supported");
+ }
+ execSQL("BEGIN EXCLUSIVE");
+ mTransactionSuccess = false;
+ mInTransaction = true;
+ }
+
+ public void beginTransactionNonExclusive() throws SQLiteBridgeException {
+ if (inTransaction()) {
+ throw new SQLiteBridgeException("Nested transactions are not supported");
+ }
+ execSQL("BEGIN IMMEDIATE");
+ mTransactionSuccess = false;
+ mInTransaction = true;
+ }
+
+ public void endTransaction() {
+ if (!inTransaction())
+ return;
+
+ try {
+ if (mTransactionSuccess) {
+ execSQL("COMMIT TRANSACTION");
+ } else {
+ execSQL("ROLLBACK TRANSACTION");
+ }
+ } catch (SQLiteBridgeException ex) {
+ Log.e(LOGTAG, "Error ending transaction", ex);
+ }
+ mInTransaction = false;
+ mTransactionSuccess = false;
+ }
+
+ public void setTransactionSuccessful() throws SQLiteBridgeException {
+ if (!inTransaction()) {
+ throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction");
+ }
+ mTransactionSuccess = true;
+ }
+
+ public boolean inTransaction() {
+ return mInTransaction;
+ }
+
+ @Override
+ public void finalize() {
+ if (isOpen()) {
+ Log.e(LOGTAG, "Bridge finalized without closing the database");
+ close();
+ }
+ }
+
+ private static void prepareWAL(final SQLiteBridge bridge) {
+ // Prepare for WAL mode. If we can, we switch to journal_mode=WAL, then
+ // set the checkpoint size appropriately. If we can't, then we fall back
+ // to truncating and synchronous writes.
+ final Cursor cursor = bridge.internalQuery("PRAGMA journal_mode=WAL", null);
+ try {
+ if (cursor.moveToFirst()) {
+ String journalMode = cursor.getString(0);
+ Log.d(LOGTAG, "Journal mode: " + journalMode);
+ if ("wal".equals(journalMode)) {
+ // Success! Let's make sure we autocheckpoint at a reasonable interval.
+ final int pageSizeBytes = bridge.getPageSizeBytes();
+ final int checkpointPageCount = MAX_WAL_SIZE_BYTES / pageSizeBytes;
+ bridge.execSQL("PRAGMA wal_autocheckpoint=" + checkpointPageCount);
+ } else {
+ if (!"truncate".equals(journalMode)) {
+ Log.w(LOGTAG, "Unable to activate WAL journal mode. Using truncate instead.");
+ bridge.execSQL("PRAGMA journal_mode=TRUNCATE");
+ }
+ Log.w(LOGTAG, "Not using WAL mode: using synchronous=FULL instead.");
+ bridge.execSQL("PRAGMA synchronous=FULL");
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private int getPageSizeBytes() {
+ if (!isOpen()) {
+ throw new IllegalStateException("Database not open.");
+ }
+
+ final Cursor cursor = internalQuery("PRAGMA page_size", null);
+ try {
+ if (!cursor.moveToFirst()) {
+ Log.w(LOGTAG, "Unable to retrieve page size.");
+ return DEFAULT_PAGE_SIZE_BYTES;
+ }
+
+ return cursor.getInt(0);
+ } finally {
+ cursor.close();
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
@@ -0,0 +1,18 @@
+/* -*- 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.sqlite;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+@WrapForJNI
+public class SQLiteBridgeException extends RuntimeException {
+ static final long serialVersionUID = 1L;
+
+ public SQLiteBridgeException() {}
+ public SQLiteBridgeException(String msg) {
+ super(msg);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java
@@ -0,0 +1,11 @@
+/* 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.util;
+
+import android.content.Intent;
+
+public interface ActivityResultHandler {
+ void onActivityResult(int resultCode, Intent data);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
@@ -0,0 +1,24 @@
+/* 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.util;
+
+import android.util.SparseArray;
+
+public final class ActivityResultHandlerMap {
+ private final SparseArray<ActivityResultHandler> mMap = new SparseArray<ActivityResultHandler>();
+ private int mCounter;
+
+ public synchronized int put(ActivityResultHandler handler) {
+ mMap.put(mCounter, handler);
+ return mCounter++;
+ }
+
+ public synchronized ActivityResultHandler getAndRemove(int i) {
+ ActivityResultHandler handler = mMap.get(i);
+ mMap.delete(i);
+
+ return handler;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java
@@ -0,0 +1,51 @@
+/* -*- 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.util;
+
+import android.app.Activity;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+public class ActivityUtils {
+ private ActivityUtils() {
+ }
+
+ public static void setFullScreen(Activity activity, boolean fullscreen) {
+ // Hide/show the system notification bar
+ Window window = activity.getWindow();
+
+ if (Versions.feature16Plus) {
+ final int newVis;
+ if (fullscreen) {
+ newVis = View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_LOW_PROFILE;
+ } else {
+ newVis = View.SYSTEM_UI_FLAG_VISIBLE;
+ }
+
+ window.getDecorView().setSystemUiVisibility(newVis);
+ } else {
+ window.setFlags(fullscreen ?
+ WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ public static boolean isFullScreen(final Activity activity) {
+ final Window window = activity.getWindow();
+
+ if (Versions.feature16Plus) {
+ final int vis = window.getDecorView().getSystemUiVisibility();
+ return (vis & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
+ }
+
+ final int flags = window.getAttributes().flags;
+ return ((flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
@@ -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.util;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.os.Bundle;
+
+@RobocopTarget
+public interface BundleEventListener {
+ /**
+ * Handles a message sent from Gecko.
+ *
+ * @param event The name of the event being sent.
+ * @param message The message data.
+ * @param callback The callback interface for this message. A callback is provided only if the
+ * originating Messaging.sendRequest call included a callback argument;
+ * otherwise, callback will be null. All listeners for a given event are given
+ * the same callback object, and exactly one listener must handle the callback.
+ */
+ void handleMessage(String event, Bundle message, EventCallback callback);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java
@@ -0,0 +1,137 @@
+/* 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.util;
+
+import java.util.concurrent.SynchronousQueue;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.util.Log;
+
+public final class Clipboard {
+ // Volatile but not synchronized: we don't care about the race condition in
+ // init, because both app contexts will be the same, but we do care about a
+ // thread having a stale null value of mContext.
+ volatile static Context mContext;
+ private final static String LOGTAG = "GeckoClipboard";
+ private final static SynchronousQueue<String> sClipboardQueue = new SynchronousQueue<String>();
+
+ private Clipboard() {
+ }
+
+ public static void init(final Context c) {
+ if (mContext != null) {
+ Log.w(LOGTAG, "Clipboard.init() called twice!");
+ return;
+ }
+ mContext = c.getApplicationContext();
+ }
+
+ @WrapForJNI(stubName = "GetClipboardTextWrapper")
+ public static String getText() {
+ // If we're on the UI thread or the background thread, we have a looper on the thread
+ // and can just call this directly. For any other threads, post the call to the
+ // background thread.
+
+ if (ThreadUtils.isOnUiThread() || ThreadUtils.isOnBackgroundThread()) {
+ return getClipboardTextImpl();
+ }
+
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ String text = getClipboardTextImpl();
+ try {
+ sClipboardQueue.put(text != null ? text : "");
+ } catch (InterruptedException ie) { }
+ }
+ });
+
+ try {
+ return sClipboardQueue.take();
+ } catch (InterruptedException ie) {
+ return "";
+ }
+ }
+
+ @WrapForJNI(stubName = "SetClipboardText")
+ public static void setText(final CharSequence text) {
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ @SuppressWarnings("deprecation")
+ public void run() {
+ // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
+ // which is a subclass of android.text.ClipboardManager.
+ if (Versions.feature11Plus) {
+ final android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ final ClipData clip = ClipData.newPlainText("Text", text);
+ try {
+ cm.setPrimaryClip(clip);
+ } catch (NullPointerException e) {
+ // Bug 776223: This is a Samsung clipboard bug. setPrimaryClip() can throw
+ // a NullPointerException if Samsung's /data/clipboard directory is full.
+ // Fortunately, the text is still successfully copied to the clipboard.
+ }
+ return;
+ }
+
+ // Deprecated.
+ android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setText(text);
+ }
+ });
+ }
+
+ /**
+ * @return true if the clipboard is nonempty, false otherwise.
+ */
+ @WrapForJNI
+ public static boolean hasText() {
+ if (Versions.feature11Plus) {
+ android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ return cm.hasPrimaryClip();
+ }
+
+ // Deprecated.
+ android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ return cm.hasText();
+ }
+
+ /**
+ * Deletes all text from the clipboard.
+ */
+ @WrapForJNI
+ public static void clearText() {
+ setText(null);
+ }
+
+ /**
+ * On some devices, access to the clipboard service needs to happen
+ * on a thread with a looper, so this function requires a looper is
+ * present on the thread.
+ */
+ @SuppressWarnings("deprecation")
+ static String getClipboardTextImpl() {
+ if (Versions.feature11Plus) {
+ android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm.hasPrimaryClip()) {
+ ClipData clip = cm.getPrimaryClip();
+ if (clip != null) {
+ ClipData.Item item = clip.getItemAt(0);
+ return item.coerceToText(mContext).toString();
+ }
+ }
+ } else {
+ android.text.ClipboardManager cm = (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm.hasText()) {
+ return cm.getText().toString();
+ }
+ }
+ return null;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java
@@ -0,0 +1,28 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class ContextUtils {
+ private ContextUtils() {}
+
+ /**
+ * @return {@link android.content.pm.PackageInfo#firstInstallTime} for the context's package.
+ * @throws PackageManager.NameNotFoundException Unexpected - we get the package name from the context so
+ * it's expected to be found.
+ */
+ public static PackageInfo getCurrentPackageInfo(final Context context) {
+ try {
+ return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new AssertionError("Should not happen: Can't get package info of own package");
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java
@@ -0,0 +1,55 @@
+/*
+ * 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.util;
+
+import android.support.annotation.NonNull;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities to help with manipulating Java's dates and calendars.
+ */
+public class DateUtil {
+ private DateUtil() {}
+
+ /**
+ * @param date the date to convert to HTTP format
+ * @return the date as specified in rfc 1123, e.g. "Tue, 01 Feb 2011 14:00:00 GMT"
+ */
+ public static String getDateInHTTPFormat(@NonNull final Date date) {
+ final DateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.US);
+ df.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return df.format(date);
+ }
+
+ /**
+ * Returns the timezone offset for the current date in minutes. See
+ * {@link #getTimezoneOffsetInMinutesForGivenDate(Calendar)} for more details.
+ */
+ public static int getTimezoneOffsetInMinutes(@NonNull final TimeZone timezone) {
+ return getTimezoneOffsetInMinutesForGivenDate(Calendar.getInstance(timezone));
+ }
+
+ /**
+ * Returns the time zone offset for the given date in minutes. The date makes a difference due to daylight
+ * savings time in some regions. We return minutes because we can accurately represent time zones that are
+ * offset by non-integer hour values, e.g. parts of New Zealand at UTC+12:45.
+ *
+ * @param calendar A calendar with the appropriate time zone & date already set.
+ */
+ public static int getTimezoneOffsetInMinutesForGivenDate(@NonNull final Calendar calendar) {
+ // via Date.getTimezoneOffset deprecated docs (note: it had incorrect order of operations).
+ // Also, we cast to int because we should never overflow here - the max should be GMT+14 = 840.
+ return (int) TimeUnit.MILLISECONDS.toMinutes(calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DrawableUtil.java
@@ -0,0 +1,58 @@
+/* -*- 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.util;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.CheckResult;
+import android.support.annotation.ColorInt;
+import android.support.annotation.ColorRes;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+
+public class DrawableUtil {
+
+ /**
+ * Tints the given drawable with the given color and returns it.
+ */
+ @CheckResult
+ public static Drawable tintDrawable(@NonNull final Context context,
+ @DrawableRes final int drawableID,
+ @ColorInt final int color) {
+ final Drawable icon = DrawableCompat.wrap(
+ ContextCompat.getDrawable(context, drawableID).mutate());
+ DrawableCompat.setTint(icon, color);
+ return icon;
+ }
+
+ /**
+ * Tints the given drawable with the given color and returns it.
+ */
+ @CheckResult
+ public static Drawable tintDrawableWithColorRes(@NonNull final Context context,
+ @DrawableRes final int drawableID,
+ @ColorRes final int colorID) {
+ return tintDrawable(context, drawableID, ContextCompat.getColor(context, colorID));
+ }
+
+ /**
+ * Tints the given drawable with the given tint list and returns it. Note that you
+ * should no longer use the argument Drawable because the argument is not mutated
+ * on pre-Lollipop devices but is mutated on L+ due to differences in the Support
+ * Library implementation (bug 1193950).
+ */
+ @CheckResult
+ public static Drawable tintDrawableWithStateList(@NonNull final Drawable drawable,
+ @NonNull final ColorStateList colorList) {
+ final Drawable wrappedDrawable = DrawableCompat.wrap(drawable.mutate());
+ DrawableCompat.setTintList(wrappedDrawable, colorList);
+ return wrappedDrawable;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
@@ -0,0 +1,29 @@
+package org.mozilla.gecko.util;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+/**
+ * Callback interface for Gecko requests.
+ *
+ * For each instance of EventCallback, exactly one of sendResponse, sendError, or sendCancel
+ * must be called to prevent observer leaks. If more than one send* method is called, or if a
+ * single send method is called multiple times, an {@link IllegalStateException} will be thrown.
+ */
+@RobocopTarget
+public interface EventCallback {
+ /**
+ * Sends a success response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ public void sendSuccess(Object response);
+
+ /**
+ * Sends an error response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ public void sendError(Object response);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
@@ -0,0 +1,259 @@
+/* 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.util;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.FilenameFilter;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Comparator;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public class FileUtils {
+ private static final String LOGTAG = "GeckoFileUtils";
+
+ /*
+ * A basic Filter for checking a filename and age.
+ **/
+ static public class NameAndAgeFilter implements FilenameFilter {
+ final private String mName;
+ final private double mMaxAge;
+
+ public NameAndAgeFilter(String name, double age) {
+ mName = name;
+ mMaxAge = age;
+ }
+
+ @Override
+ public boolean accept(File dir, String filename) {
+ if (mName == null || mName.matches(filename)) {
+ File f = new File(dir, filename);
+
+ if (mMaxAge < 0 || System.currentTimeMillis() - f.lastModified() > mMaxAge) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ @RobocopTarget
+ public static void delTree(File dir, FilenameFilter filter, boolean recurse) {
+ String[] files = null;
+
+ if (filter != null) {
+ files = dir.list(filter);
+ } else {
+ files = dir.list();
+ }
+
+ if (files == null) {
+ return;
+ }
+
+ for (String file : files) {
+ File f = new File(dir, file);
+ delete(f, recurse);
+ }
+ }
+
+ public static boolean delete(File file) throws IOException {
+ return delete(file, true);
+ }
+
+ public static boolean delete(File file, boolean recurse) {
+ if (file.isDirectory() && recurse) {
+ // If the quick delete failed and this is a dir, recursively delete the contents of the dir
+ String files[] = file.list();
+ for (String temp : files) {
+ File fileDelete = new File(file, temp);
+ try {
+ delete(fileDelete);
+ } catch (IOException ex) {
+ Log.i(LOGTAG, "Error deleting " + fileDelete.getPath(), ex);
+ }
+ }
+ }
+
+ // Even if this is a dir, it should now be empty and delete should work
+ return file.delete();
+ }
+
+ /**
+ * A generic solution to read a JSONObject from a file. See
+ * {@link #readStringFromFile(File)} for more details.
+ *
+ * @throws IOException if the file is empty, or another IOException occurs
+ * @throws JSONException if the file could not be converted to a JSONObject.
+ */
+ public static JSONObject readJSONObjectFromFile(final File file) throws IOException, JSONException {
+ if (file.length() == 0) {
+ // Redirect this exception so it's clearer than when the JSON parser catches it.
+ throw new IOException("Given file is empty - the JSON parser cannot create an object from an empty file");
+ }
+ return new JSONObject(readStringFromFile(file));
+ }
+
+ /**
+ * A generic solution to read from a file. For more details,
+ * see {@link #readStringFromInputStreamAndCloseStream(InputStream, int)}.
+ *
+ * This method loads the entire file into memory so will have the expected performance impact.
+ * If you're trying to read a large file, you should be handling your own reading to avoid
+ * out-of-memory errors.
+ */
+ public static String readStringFromFile(final File file) throws IOException {
+ // FileInputStream will throw FileNotFoundException if the file does not exist, but
+ // File.length will return 0 if the file does not exist so we catch it sooner.
+ if (!file.exists()) {
+ throw new FileNotFoundException("Given file, " + file + ", does not exist");
+ } else if (file.length() == 0) {
+ return "";
+ }
+ final int len = (int) file.length(); // includes potential EOF character.
+ return readStringFromInputStreamAndCloseStream(new FileInputStream(file), len);
+ }
+
+ /**
+ * A generic solution to read from an input stream in UTF-8. This function will read from the stream until it
+ * is finished and close the stream - this is necessary to close the wrapping resources.
+ *
+ * For a higher-level method, see {@link #readStringFromFile(File)}.
+ *
+ * Since this is generic, it may not be the most performant for your use case.
+ *
+ * @param bufferSize Size of the underlying buffer for read optimizations - must be > 0.
+ */
+ public static String readStringFromInputStreamAndCloseStream(final InputStream inputStream, final int bufferSize)
+ throws IOException {
+ if (bufferSize <= 0) {
+ // Safe close: it's more important to alert the programmer of
+ // their error than to let them catch and continue on their way.
+ IOUtils.safeStreamClose(inputStream);
+ throw new IllegalArgumentException("Expected buffer size larger than 0. Got: " + bufferSize);
+ }
+
+ final StringBuilder stringBuilder = new StringBuilder(bufferSize);
+ final InputStreamReader reader = new InputStreamReader(inputStream, Charset.forName("UTF-8"));
+ try {
+ int charsRead;
+ final char[] buffer = new char[bufferSize];
+ while ((charsRead = reader.read(buffer, 0, bufferSize)) != -1) {
+ stringBuilder.append(buffer, 0, charsRead);
+ }
+ } finally {
+ reader.close();
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * A generic solution to write a JSONObject to a file.
+ * See {@link #writeStringToFile(File, String)} for more details.
+ */
+ public static void writeJSONObjectToFile(final File file, final JSONObject obj) throws IOException {
+ writeStringToFile(file, obj.toString());
+ }
+
+ /**
+ * A generic solution to write to a File - the given file will be overwritten. If it does not exist yet, it will
+ * be created. See {@link #writeStringToOutputStreamAndCloseStream(OutputStream, String)} for more details.
+ */
+ public static void writeStringToFile(final File file, final String str) throws IOException {
+ writeStringToOutputStreamAndCloseStream(new FileOutputStream(file, false), str);
+ }
+
+ /**
+ * A generic solution to write to an output stream in UTF-8. The stream will be closed at the
+ * completion of this method - it's necessary in order to close the wrapping resources.
+ *
+ * For a higher-level method, see {@link #writeStringToFile(File, String)}.
+ *
+ * Since this is generic, it may not be the most performant for your use case.
+ */
+ public static void writeStringToOutputStreamAndCloseStream(final OutputStream outputStream, final String str)
+ throws IOException {
+ try {
+ final OutputStreamWriter writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"));
+ try {
+ writer.write(str);
+ } finally {
+ writer.close();
+ }
+ } finally {
+ // OutputStreamWriter.close can throw before closing the
+ // underlying stream. For safety, we close here too.
+ outputStream.close();
+ }
+ }
+
+ public static class FilenameWhitelistFilter implements FilenameFilter {
+ private final Set<String> mFilenameWhitelist;
+
+ public FilenameWhitelistFilter(final Set<String> filenameWhitelist) {
+ mFilenameWhitelist = filenameWhitelist;
+ }
+
+ @Override
+ public boolean accept(final File dir, final String filename) {
+ return mFilenameWhitelist.contains(filename);
+ }
+ }
+
+ public static class FilenameRegexFilter implements FilenameFilter {
+ private final Pattern mPattern;
+
+ // Each time `Pattern.matcher` is called, a new matcher is created. We can avoid the excessive object creation
+ // by caching the returned matcher and calling `Matcher.reset` on it. Since Matcher's are not thread safe,
+ // this assumes `FilenameFilter.accept` is not run in parallel (which, according to the source, it is not).
+ private Matcher mCachedMatcher;
+
+ public FilenameRegexFilter(final Pattern pattern) {
+ mPattern = pattern;
+ }
+
+ @Override
+ public boolean accept(final File dir, final String filename) {
+ if (mCachedMatcher == null) {
+ mCachedMatcher = mPattern.matcher(filename);
+ } else {
+ mCachedMatcher.reset(filename);
+ }
+ return mCachedMatcher.matches();
+ }
+ }
+
+ public static class FileLastModifiedComparator implements Comparator<File> {
+ @Override
+ public int compare(final File lhs, final File rhs) {
+ // Long.compare is API 19+.
+ final long lhsModified = lhs.lastModified();
+ final long rhsModified = rhs.lastModified();
+ if (lhsModified < rhsModified) {
+ return -1;
+ } else if (lhsModified == rhsModified) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java
@@ -0,0 +1,43 @@
+/* -*- 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.util;
+
+import android.graphics.PointF;
+
+import java.lang.IllegalArgumentException;
+
+public final class FloatUtils {
+ private FloatUtils() {}
+
+ public static boolean fuzzyEquals(float a, float b) {
+ return (Math.abs(a - b) < 1e-6);
+ }
+
+ public static boolean fuzzyEquals(PointF a, PointF b) {
+ return fuzzyEquals(a.x, b.x) && fuzzyEquals(a.y, b.y);
+ }
+
+ /*
+ * Returns the value that represents a linear transition between `from` and `to` at time `t`,
+ * which is on the scale [0, 1). Thus with t = 0.0f, this returns `from`; with t = 1.0f, this
+ * returns `to`; with t = 0.5f, this returns the value halfway from `from` to `to`.
+ */
+ public static float interpolate(float from, float to, float t) {
+ return from + (to - from) * t;
+ }
+
+ /**
+ * Returns 'value', clamped so that it isn't any lower than 'low', and it
+ * isn't any higher than 'high'.
+ */
+ public static float clamp(float value, float low, float high) {
+ if (high < low) {
+ throw new IllegalArgumentException(
+ "clamp called with invalid parameters (" + high + " < " + low + ")" );
+ }
+ return Math.max(low, Math.min(high, value));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java
@@ -0,0 +1,140 @@
+/* -*- 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.util;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public final class GamepadUtils {
+ private static final int SONY_XPERIA_GAMEPAD_DEVICE_ID = 196611;
+
+ private static View.OnKeyListener sClickDispatcher;
+ private static float sDeadZoneThresholdOverride = 1e-2f;
+
+ private GamepadUtils() {
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+ private static boolean isGamepadKey(KeyEvent event) {
+ if (Build.VERSION.SDK_INT < 12) {
+ return false;
+ }
+ return (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
+ }
+
+ public static boolean isActionKey(KeyEvent event) {
+ return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_A));
+ }
+
+ public static boolean isActionKeyDown(KeyEvent event) {
+ return isActionKey(event) && event.getAction() == KeyEvent.ACTION_DOWN;
+ }
+
+ public static boolean isBackKey(KeyEvent event) {
+ return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B));
+ }
+
+ public static void overrideDeadZoneThreshold(float threshold) {
+ sDeadZoneThresholdOverride = threshold;
+ }
+
+ public static boolean isValueInDeadZone(MotionEvent event, int axis) {
+ float threshold;
+ if (sDeadZoneThresholdOverride >= 0) {
+ threshold = sDeadZoneThresholdOverride;
+ } else {
+ InputDevice.MotionRange range = event.getDevice().getMotionRange(axis);
+ threshold = range.getFlat() + range.getFuzz();
+ }
+ float value = event.getAxisValue(axis);
+ return (Math.abs(value) < threshold);
+ }
+
+ public static boolean isPanningControl(MotionEvent event) {
+ if (Build.VERSION.SDK_INT < 12) {
+ return false;
+ }
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_MASK) != InputDevice.SOURCE_CLASS_JOYSTICK) {
+ return false;
+ }
+ if (isValueInDeadZone(event, MotionEvent.AXIS_X)
+ && isValueInDeadZone(event, MotionEvent.AXIS_Y)
+ && isValueInDeadZone(event, MotionEvent.AXIS_Z)
+ && isValueInDeadZone(event, MotionEvent.AXIS_RZ)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static View.OnKeyListener getClickDispatcher() {
+ if (sClickDispatcher == null) {
+ sClickDispatcher = new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (isActionKeyDown(event)) {
+ return v.performClick();
+ }
+ return false;
+ }
+ };
+ }
+ return sClickDispatcher;
+ }
+
+ public static KeyEvent translateSonyXperiaGamepadKeys(int keyCode, KeyEvent event) {
+ // The cross and circle button mappings may be swapped in the different regions so
+ // determine if they are swapped so the proper key codes can be mapped to the keys
+ boolean areKeysSwapped = areSonyXperiaGamepadKeysSwapped();
+
+ // If a Sony Xperia, remap the cross and circle buttons to buttons
+ // A and B for the gamepad API
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ keyCode = (areKeysSwapped ? KeyEvent.KEYCODE_BUTTON_A : KeyEvent.KEYCODE_BUTTON_B);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ keyCode = (areKeysSwapped ? KeyEvent.KEYCODE_BUTTON_B : KeyEvent.KEYCODE_BUTTON_A);
+ break;
+
+ default:
+ return event;
+ }
+
+ return new KeyEvent(event.getAction(), keyCode);
+ }
+
+ public static boolean isSonyXperiaGamepadKeyEvent(KeyEvent event) {
+ return (event.getDeviceId() == SONY_XPERIA_GAMEPAD_DEVICE_ID &&
+ "Sony Ericsson".equals(Build.MANUFACTURER) &&
+ ("R800".equals(Build.MODEL) || "R800i".equals(Build.MODEL)));
+ }
+
+ private static boolean areSonyXperiaGamepadKeysSwapped() {
+ // The cross and circle buttons on Sony Xperia phones are swapped
+ // in different regions
+ // http://developer.sonymobile.com/2011/02/13/xperia-play-game-keys/
+ final char DEFAULT_O_BUTTON_LABEL = 0x25CB;
+
+ boolean swapped = false;
+ int[] deviceIds = InputDevice.getDeviceIds();
+
+ for (int i = 0; deviceIds != null && i < deviceIds.length; i++) {
+ KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(deviceIds[i]);
+ if (keyCharacterMap != null && DEFAULT_O_BUTTON_LABEL ==
+ keyCharacterMap.getDisplayLabel(KeyEvent.KEYCODE_DPAD_CENTER)) {
+ swapped = true;
+ break;
+ }
+ }
+ return swapped;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
@@ -0,0 +1,76 @@
+/* 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.util;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.SynchronousQueue;
+
+final class GeckoBackgroundThread extends Thread {
+ private static final String LOOPER_NAME = "GeckoBackgroundThread";
+
+ // Guarded by 'GeckoBackgroundThread.class'.
+ private static Handler handler;
+ private static Thread thread;
+
+ // The initial Runnable to run on the new thread. Its purpose
+ // is to avoid us having to wait for the new thread to start.
+ private Runnable initialRunnable;
+
+ // Singleton, so private constructor.
+ private GeckoBackgroundThread(final Runnable initialRunnable) {
+ this.initialRunnable = initialRunnable;
+ }
+
+ @Override
+ public void run() {
+ setName(LOOPER_NAME);
+ Looper.prepare();
+
+ synchronized (GeckoBackgroundThread.class) {
+ handler = new Handler();
+ GeckoBackgroundThread.class.notify();
+ }
+
+ if (initialRunnable != null) {
+ initialRunnable.run();
+ initialRunnable = null;
+ }
+
+ Looper.loop();
+ }
+
+ private static void startThread(final Runnable initialRunnable) {
+ thread = new GeckoBackgroundThread(initialRunnable);
+ ThreadUtils.setBackgroundThread(thread);
+
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ // Get a Handler for a looper thread, or create one if it doesn't yet exist.
+ /*package*/ static synchronized Handler getHandler() {
+ if (thread == null) {
+ startThread(null);
+ }
+
+ while (handler == null) {
+ try {
+ GeckoBackgroundThread.class.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ return handler;
+ }
+
+ /*package*/ static synchronized void post(final Runnable runnable) {
+ if (thread == null) {
+ startThread(runnable);
+ return;
+ }
+ getHandler().post(runnable);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java
@@ -0,0 +1,14 @@
+/* -*- 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.util;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+@RobocopTarget
+public interface GeckoEventListener {
+ void handleMessage(String event, JSONObject message);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java
@@ -0,0 +1,267 @@
+/* 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.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.Log;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.mozglue.NativeZip;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Stack;
+
+/* Reads out of a multiple level deep jar file such as
+ * jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
+ */
+public final class GeckoJarReader {
+ private static final String LOGTAG = "GeckoJarReader";
+
+ private GeckoJarReader() {}
+
+ public static Bitmap getBitmap(Context context, Resources resources, String url) {
+ BitmapDrawable drawable = getBitmapDrawable(context, resources, url);
+ return (drawable != null) ? drawable.getBitmap() : null;
+ }
+
+ public static BitmapDrawable getBitmapDrawable(Context context, Resources resources,
+ String url) {
+ Stack<String> jarUrls = parseUrl(url);
+ InputStream inputStream = null;
+ BitmapDrawable bitmap = null;
+
+ NativeZip zip = null;
+ try {
+ // Load the initial jar file as a zip
+ zip = getZipFile(context, jarUrls.pop());
+ inputStream = getStream(zip, jarUrls, url);
+ if (inputStream != null) {
+ bitmap = new BitmapDrawable(resources, inputStream);
+ // BitmapDrawable created from a stream does not set the correct target density from resources.
+ // In fact it discards the resources https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/graphics/java/android/graphics/drawable/BitmapDrawable.java#191
+ bitmap.setTargetDensity(resources.getDisplayMetrics());
+ }
+ } catch (IOException | URISyntaxException ex) {
+ Log.e(LOGTAG, "Exception ", ex);
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException ex) {
+ Log.e(LOGTAG, "Error closing stream", ex);
+ }
+ }
+ if (zip != null) {
+ zip.close();
+ }
+ }
+
+ return bitmap;
+ }
+
+ public static String getText(Context context, String url) {
+ Stack<String> jarUrls = parseUrl(url);
+
+ NativeZip zip = null;
+ BufferedReader reader = null;
+ String text = null;
+ try {
+ zip = getZipFile(context, jarUrls.pop());
+ InputStream input = getStream(zip, jarUrls, url);
+ if (input != null) {
+ reader = new BufferedReader(new InputStreamReader(input));
+ text = reader.readLine();
+ }
+ } catch (IOException | URISyntaxException ex) {
+ Log.e(LOGTAG, "Exception ", ex);
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ex) {
+ Log.e(LOGTAG, "Error closing reader", ex);
+ }
+ }
+ if (zip != null) {
+ zip.close();
+ }
+ }
+
+ return text;
+ }
+
+ private static NativeZip getZipFile(Context context, String url)
+ throws IOException, URISyntaxException {
+ URI fileUrl = new URI(url);
+ GeckoLoader.loadMozGlue(context);
+ return new NativeZip(fileUrl.getPath());
+ }
+
+ @RobocopTarget
+ /**
+ * Extract a (possibly nested) file from an archive and write it to a temporary file.
+ *
+ * @param context Android context.
+ * @param url to open. Can include jar: to "reach into" nested archives.
+ * @param dir to write temporary file to.
+ * @return a <code>File</code>, if one could be written; otherwise null.
+ * @throws IOException if an error occured.
+ */
+ public static File extractStream(Context context, String url, File dir, String suffix) throws IOException {
+ InputStream input = null;
+ try {
+ try {
+ final URI fileURI = new URI(url);
+ // We don't check the scheme because we want to catch bare files, not just file:// URIs.
+ // If we let bare files through, we'd try to open them as ZIP files later -- and crash in native code.
+ if (fileURI != null && fileURI.getPath() != null) {
+ final File inputFile = new File(fileURI.getPath());
+ if (inputFile != null && inputFile.exists()) {
+ input = new FileInputStream(inputFile);
+ }
+ }
+ } catch (URISyntaxException e) {
+ // Not a file:// URI.
+ }
+ if (input == null) {
+ // No luck with file:// URI; maybe some other URI?
+ input = getStream(context, url);
+ }
+ if (input == null) {
+ // Not found!
+ return null;
+ }
+
+ // n.b.: createTempFile does not in fact delete the file.
+ final File file = File.createTempFile("extractStream", suffix, dir);
+ OutputStream output = null;
+ try {
+ output = new FileOutputStream(file);
+ byte[] buf = new byte[8192];
+ int len;
+ while ((len = input.read(buf)) >= 0) {
+ output.write(buf, 0, len);
+ }
+ return file;
+ } finally {
+ if (output != null) {
+ output.close();
+ }
+ }
+ } finally {
+ if (input != null) {
+ try {
+ input.close();
+ } catch (IOException e) {
+ Log.w(LOGTAG, "Got exception closing stream; ignoring.", e);
+ }
+ }
+ }
+ }
+
+ @RobocopTarget
+ public static InputStream getStream(Context context, String url) {
+ Stack<String> jarUrls = parseUrl(url);
+ try {
+ NativeZip zip = getZipFile(context, jarUrls.pop());
+ return getStream(zip, jarUrls, url);
+ } catch (Exception ex) {
+ // Some JNI code throws IllegalArgumentException on a bad file name;
+ // swallow the error and return null. We could also see legitimate
+ // IOExceptions here.
+ Log.e(LOGTAG, "Exception getting input stream from jar URL: " + url, ex);
+ return null;
+ }
+ }
+
+ private static InputStream getStream(NativeZip zip, Stack<String> jarUrls, String origUrl) {
+ InputStream inputStream = null;
+
+ // loop through children jar files until we reach the innermost one
+ while (!jarUrls.empty()) {
+ String fileName = jarUrls.pop();
+
+ if (inputStream != null) {
+ // intermediate NativeZips and InputStreams will be garbage collected.
+ try {
+ zip = new NativeZip(inputStream);
+ } catch (IllegalArgumentException e) {
+ String description = "!!! BUG 849589 !!! origUrl=" + origUrl;
+ Log.e(LOGTAG, description, e);
+ throw new IllegalArgumentException(description);
+ }
+ }
+
+ inputStream = zip.getInputStream(fileName);
+ if (inputStream == null) {
+ Log.d(LOGTAG, "No Entry for " + fileName);
+ return null;
+ }
+ }
+
+ return inputStream;
+ }
+
+ /* Returns a stack of strings breaking the url up into pieces. Each piece
+ * is assumed to point to a jar file except for the final one. Callers should
+ * pass in the url to parse, and null for the parent parameter (used for recursion)
+ * For example, jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
+ * will return:
+ * file:///data/app/org.mozilla.fennec.apk
+ * omni.ja
+ * chrome/chrome/content/branding/favicon32.png
+ */
+ private static Stack<String> parseUrl(String url) {
+ return parseUrl(url, null);
+ }
+
+ private static Stack<String> parseUrl(String url, Stack<String> results) {
+ if (results == null) {
+ results = new Stack<String>();
+ }
+
+ if (url.startsWith("jar:")) {
+ int jarEnd = url.lastIndexOf("!");
+ String subStr = url.substring(4, jarEnd);
+ results.push(url.substring(jarEnd + 2)); // remove the !/ characters
+ return parseUrl(subStr, results);
+ } else {
+ results.push(url);
+ return results;
+ }
+ }
+
+ public static String getJarURL(Context context, String pathInsideJAR) {
+ // We need to encode the package resource path, because it might contain illegal characters. For example:
+ // /mnt/asec2/[2]org.mozilla.fennec-1/pkg.apk
+ // The round-trip through a URI does this for us.
+ final String resourcePath = context.getPackageResourcePath();
+ return computeJarURI(resourcePath, pathInsideJAR);
+ }
+
+ /**
+ * Encodes its resource path correctly.
+ */
+ @RobocopTarget
+ public static String computeJarURI(String resourcePath, String pathInsideJAR) {
+ final String resURI = new File(resourcePath).toURI().toString();
+
+ // TODO: do we need to encode the file path, too?
+ return "jar:jar:" + resURI + "!/" + AppConstants.OMNIJAR_NAME + "!/" + pathInsideJAR;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java
@@ -0,0 +1,94 @@
+/* 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.util;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.util.Log;
+
+public abstract class GeckoRequest {
+ private static final String LOGTAG = "GeckoRequest";
+ private static final AtomicInteger currentId = new AtomicInteger(0);
+
+ private final int id = currentId.getAndIncrement();
+ private final String name;
+ private final String data;
+
+ /**
+ * Creates a request that can be dispatched using
+ * {@link GeckoAppShell#sendRequestToGecko(GeckoRequest)}.
+ *
+ * @param name The name of the event associated with this request, which must have a
+ * Gecko-side listener registered to respond to this request.
+ * @param data Data to send with this request, which can be any object serializable by
+ * {@link JSONObject#put(String, Object)}.
+ */
+ @RobocopTarget
+ public GeckoRequest(String name, Object data) {
+ this.name = name;
+ final JSONObject message = new JSONObject();
+ try {
+ message.put("id", id);
+ message.put("data", data);
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "JSON error", e);
+ }
+ this.data = message.toString();
+ }
+
+ /**
+ * Gets the ID for this request.
+ *
+ * @return The request ID
+ */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Gets the event name associated with this request.
+ *
+ * @return The name of the event sent to Gecko
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the stringified data associated with this request.
+ *
+ * @return The data being sent with the request
+ */
+ public String getData() {
+ return data;
+ }
+
+ /**
+ * Callback executed when the request succeeds.
+ *
+ * @param nativeJSObject The response data from Gecko
+ */
+ @RobocopTarget
+ public abstract void onResponse(NativeJSObject nativeJSObject);
+
+ /**
+ * Callback executed when the request fails.
+ *
+ * By default, an exception is thrown. This should be overridden if the
+ * GeckoRequest is able to recover from the error.
+ *
+ * @throws RuntimeException
+ */
+ @RobocopTarget
+ public void onError(NativeJSObject error) {
+ final String message = error.optString("message", "<no message>");
+ final String stack = error.optString("stack", "<no stack>");
+ throw new RuntimeException("Unhandled error for GeckoRequest " + name + ": " + message + "\nJS stack:\n" + stack);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
@@ -0,0 +1,159 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.util.Log;
+
+public final class HardwareCodecCapabilityUtils {
+ private static final String LOGTAG = "GeckoHardwareCodecCapabilityUtils";
+
+ // List of supported HW VP8 encoders.
+ private static final String[] supportedVp8HwEncCodecPrefixes =
+ {"OMX.qcom.", "OMX.Intel." };
+ // List of supported HW VP8 decoders.
+ private static final String[] supportedVp8HwDecCodecPrefixes =
+ {"OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "OMX.Intel." };
+ private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
+ // NV12 color format supported by QCOM codec, but not declared in MediaCodec -
+ // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
+ private static final int
+ COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
+ // Allowable color formats supported by codec - in order of preference.
+ private static final int[] supportedColorList = {
+ CodecCapabilities.COLOR_FormatYUV420Planar,
+ CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
+ CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
+ COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
+ };
+
+ @WrapForJNI(allowMultithread = true, stubName = "FindDecoderCodecInfoForMimeType")
+ public static boolean findDecoderCodecInfoForMimeType(String aMimeType) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (info.isEncoder()) {
+ continue;
+ }
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(aMimeType)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean getHWEncoderCapability() {
+ if (Versions.feature20Plus) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (!info.isEncoder()) {
+ continue;
+ }
+ String name = null;
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(VP8_MIME_TYPE)) {
+ name = info.getName();
+ break;
+ }
+ }
+ if (name == null) {
+ continue; // No HW support in this codec; try the next one.
+ }
+ Log.e(LOGTAG, "Found candidate encoder " + name);
+
+ // Check if this is supported encoder.
+ boolean supportedCodec = false;
+ for (String codecPrefix : supportedVp8HwEncCodecPrefixes) {
+ if (name.startsWith(codecPrefix)) {
+ supportedCodec = true;
+ break;
+ }
+ }
+ if (!supportedCodec) {
+ continue;
+ }
+
+ // Check if codec supports either yuv420 or nv12.
+ CodecCapabilities capabilities =
+ info.getCapabilitiesForType(VP8_MIME_TYPE);
+ for (int colorFormat : capabilities.colorFormats) {
+ Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
+ }
+ for (int supportedColorFormat : supportedColorList) {
+ for (int codecColorFormat : capabilities.colorFormats) {
+ if (codecColorFormat == supportedColorFormat) {
+ // Found supported HW Encoder.
+ Log.e(LOGTAG, "Found target encoder " + name +
+ ". Color: 0x" + Integer.toHexString(codecColorFormat));
+ return true;
+ }
+ }
+ }
+ }
+ }
+ // No HW encoder.
+ return false;
+ }
+
+ public static boolean getHWDecoderCapability() {
+ if (Versions.feature20Plus) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (info.isEncoder()) {
+ continue;
+ }
+ String name = null;
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(VP8_MIME_TYPE)) {
+ name = info.getName();
+ break;
+ }
+ }
+ if (name == null) {
+ continue; // No HW support in this codec; try the next one.
+ }
+ Log.e(LOGTAG, "Found candidate decoder " + name);
+
+ // Check if this is supported decoder.
+ boolean supportedCodec = false;
+ for (String codecPrefix : supportedVp8HwDecCodecPrefixes) {
+ if (name.startsWith(codecPrefix)) {
+ supportedCodec = true;
+ break;
+ }
+ }
+ if (!supportedCodec) {
+ continue;
+ }
+
+ // Check if codec supports either yuv420 or nv12.
+ CodecCapabilities capabilities =
+ info.getCapabilitiesForType(VP8_MIME_TYPE);
+ for (int colorFormat : capabilities.colorFormats) {
+ Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
+ }
+ for (int supportedColorFormat : supportedColorList) {
+ for (int codecColorFormat : capabilities.colorFormats) {
+ if (codecColorFormat == supportedColorFormat) {
+ // Found supported HW decoder.
+ Log.e(LOGTAG, "Found target decoder " + name +
+ ". Color: 0x" + Integer.toHexString(codecColorFormat));
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false; // No HW decoder.
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
@@ -0,0 +1,109 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.SysInfo;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.util.Log;
+import android.view.ViewConfiguration;
+
+public final class HardwareUtils {
+ private static final String LOGTAG = "GeckoHardwareUtils";
+
+ private static final boolean IS_AMAZON_DEVICE = Build.MANUFACTURER.equalsIgnoreCase("Amazon");
+ public static final boolean IS_KINDLE_DEVICE = IS_AMAZON_DEVICE &&
+ (Build.MODEL.equals("Kindle Fire") ||
+ Build.MODEL.startsWith("KF"));
+
+ private static volatile boolean sInited;
+
+ // These are all set once, during init.
+ private static volatile boolean sIsLargeTablet;
+ private static volatile boolean sIsSmallTablet;
+ private static volatile boolean sIsTelevision;
+
+ private HardwareUtils() {
+ }
+
+ public static void init(Context context) {
+ if (sInited) {
+ // This is unavoidable, given that HardwareUtils is called from background services.
+ Log.d(LOGTAG, "HardwareUtils already inited.");
+ return;
+ }
+
+ // Pre-populate common flags from the context.
+ final int screenLayoutSize = context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+ if (Build.VERSION.SDK_INT >= 11) {
+ if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ sIsLargeTablet = true;
+ } else if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE) {
+ sIsSmallTablet = true;
+ }
+ if (Build.VERSION.SDK_INT >= 16) {
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
+ sIsTelevision = true;
+ }
+ }
+ }
+
+ sInited = true;
+ }
+
+ public static boolean isTablet() {
+ return sIsLargeTablet || sIsSmallTablet;
+ }
+
+ public static boolean isLargeTablet() {
+ return sIsLargeTablet;
+ }
+
+ public static boolean isSmallTablet() {
+ return sIsSmallTablet;
+ }
+
+ public static boolean isTelevision() {
+ return sIsTelevision;
+ }
+
+ public static int getMemSize() {
+ return SysInfo.getMemSize();
+ }
+
+ /**
+ * @return false if the current system is not supported (e.g. APK/system ABI mismatch).
+ */
+ public static boolean isSupportedSystem() {
+ if (Build.VERSION.SDK_INT < AppConstants.Versions.MIN_SDK_VERSION ||
+ Build.VERSION.SDK_INT > AppConstants.Versions.MAX_SDK_VERSION) {
+ return false;
+ }
+
+ // See http://developer.android.com/ndk/guides/abis.html
+ boolean isSystemARM = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("arm");
+ boolean isSystemX86 = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("x86");
+
+ boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("arm");
+ boolean isAppX86 = AppConstants.ANDROID_CPU_ARCH.startsWith("x86");
+
+ // Only reject known incompatible ABIs. Better safe than sorry.
+ if ((isSystemX86 && isAppARM) || (isSystemARM && isAppX86)) {
+ return false;
+ }
+
+ if ((isSystemX86 && isAppX86) || (isSystemARM && isAppARM)) {
+ return true;
+ }
+
+ Log.w(LOGTAG, "Unknown app/system ABI combination: " + AppConstants.MOZ_APP_ABI + " / " + Build.CPU_ABI);
+ return true;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java
@@ -0,0 +1,176 @@
+/* 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.util;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+public final class INIParser extends INISection {
+ // default file to read and write to
+ private final File mFile;
+
+ // List of sections in the current iniFile. null if the file has not been parsed yet
+ private Hashtable<String, INISection> mSections;
+
+ // create a parser. The file will not be read until you attempt to
+ // access sections or properties inside it. At that point its read synchronously
+ public INIParser(File iniFile) {
+ super("");
+ mFile = iniFile;
+ }
+
+ // write ini data to the default file. Will overwrite anything current inside
+ public void write() {
+ writeTo(mFile);
+ }
+
+ // write to the specified file. Will overwrite anything current inside
+ public void writeTo(File f) {
+ if (f == null)
+ return;
+
+ FileWriter outputStream = null;
+ try {
+ outputStream = new FileWriter(f);
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+
+ BufferedWriter writer = new BufferedWriter(outputStream);
+ try {
+ write(writer);
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void write(BufferedWriter writer) throws IOException {
+ super.write(writer);
+
+ if (mSections != null) {
+ for (Enumeration<INISection> e = mSections.elements(); e.hasMoreElements();) {
+ INISection section = e.nextElement();
+ section.write(writer);
+ writer.newLine();
+ }
+ }
+ }
+
+ // return all of the sections inside this file
+ public Hashtable<String, INISection> getSections() {
+ if (mSections == null) {
+ try {
+ parse();
+ } catch (IOException e) {
+ debug("Error parsing: " + e);
+ }
+ }
+ return mSections;
+ }
+
+ // parse the default file
+ @Override
+ protected void parse() throws IOException {
+ super.parse();
+ parse(mFile);
+ }
+
+ // parse a passed in file
+ private void parse(File f) throws IOException {
+ // Set up internal data members
+ mSections = new Hashtable<String, INISection>();
+
+ if (f == null || !f.exists())
+ return;
+
+ FileReader inputStream = null;
+ try {
+ inputStream = new FileReader(f);
+ } catch (FileNotFoundException e1) {
+ // If the file doesn't exist. Just return;
+ return;
+ }
+
+ BufferedReader buf = new BufferedReader(inputStream);
+ String line = null; // current line of text we are parsing
+ INISection currentSection = null; // section we are currently parsing
+
+ while ((line = buf.readLine()) != null) {
+
+ if (line != null)
+ line = line.trim();
+
+ // blank line or a comment. ignore it
+ if (line == null || line.length() == 0 || line.charAt(0) == ';') {
+ debug("Ignore line: " + line);
+ } else if (line.charAt(0) == '[') {
+ debug("Parse as section: " + line);
+ currentSection = new INISection(line.substring(1, line.length() - 1));
+ mSections.put(currentSection.getName(), currentSection);
+ } else {
+ debug("Parse as property: " + line);
+
+ String[] pieces = line.split("=");
+ if (pieces.length != 2)
+ continue;
+
+ String key = pieces[0].trim();
+ String value = pieces[1].trim();
+ if (currentSection != null) {
+ currentSection.setProperty(key, value);
+ } else {
+ mProperties.put(key, value);
+ }
+ }
+ }
+ buf.close();
+ }
+
+ // add a section to the file
+ public void addSection(INISection sect) {
+ // ensure that we have parsed the file
+ getSections();
+ mSections.put(sect.getName(), sect);
+ }
+
+ // get a section from the file. will return null if the section doesn't exist
+ public INISection getSection(String key) {
+ // ensure that we have parsed the file
+ getSections();
+ return mSections.get(key);
+ }
+
+ // remove an entire section from the file
+ public void removeSection(String name) {
+ // ensure that we have parsed the file
+ getSections();
+ mSections.remove(name);
+ }
+
+ // rename a section; nuking any previous section with the new
+ // name in the process
+ public void renameSection(String oldName, String newName) {
+ // ensure that we have parsed the file
+ getSections();
+
+ mSections.remove(newName);
+ INISection section = mSections.get(oldName);
+ if (section == null)
+ return;
+
+ section.setName(newName);
+ mSections.remove(oldName);
+ mSections.put(newName, section);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java
@@ -0,0 +1,123 @@
+/* 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.util;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+public class INISection {
+ private static final String LOGTAG = "INIParser";
+
+ // default file to read and write to
+ private String mName;
+ public String getName() { return mName; }
+ public void setName(String name) { mName = name; }
+
+ // show or hide debug logging
+ private boolean mDebug;
+
+ // Global properties that aren't inside a section in the file
+ protected Hashtable<String, Object> mProperties;
+
+ // create a parser. The file will not be read until you attempt to
+ // access sections or properties inside it. At that point its read synchronously
+ public INISection(String name) {
+ mName = name;
+ }
+
+ // log a debug string to the console
+ protected void debug(String msg) {
+ if (mDebug) {
+ Log.i(LOGTAG, msg);
+ }
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public Object getProperty(String key) {
+ getProperties(); // ensure that we have parsed the file
+ return mProperties.get(key);
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public int getIntProperty(String key) {
+ Object val = getProperty(key);
+ if (val == null)
+ return -1;
+
+ return Integer.parseInt(val.toString());
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public String getStringProperty(String key) {
+ Object val = getProperty(key);
+ if (val == null)
+ return null;
+
+ return val.toString();
+ }
+
+ // get a hashtable of all the global properties in this file
+ public Hashtable<String, Object> getProperties() {
+ if (mProperties == null) {
+ try {
+ parse();
+ } catch (IOException e) {
+ debug("Error parsing: " + e);
+ }
+ }
+ return mProperties;
+ }
+
+ // do nothing for generic sections
+ protected void parse() throws IOException {
+ mProperties = new Hashtable<String, Object>();
+ }
+
+ // set a property. Will erase the property if value = null
+ public void setProperty(String key, Object value) {
+ getProperties(); // ensure that we have parsed the file
+ if (value == null)
+ removeProperty(key);
+ else
+ mProperties.put(key.trim(), value);
+ }
+
+ // remove a property
+ public void removeProperty(String name) {
+ // ensure that we have parsed the file
+ getProperties();
+ mProperties.remove(name);
+ }
+
+ public void write(BufferedWriter writer) throws IOException {
+ if (!TextUtils.isEmpty(mName)) {
+ writer.write("[" + mName + "]");
+ writer.newLine();
+ }
+
+ if (mProperties != null) {
+ for (Enumeration<String> e = mProperties.keys(); e.hasMoreElements();) {
+ String key = e.nextElement();
+ writeProperty(writer, key, mProperties.get(key));
+ }
+ }
+ writer.newLine();
+ }
+
+ // Helper function to write out a property
+ private void writeProperty(BufferedWriter writer, String key, Object value) {
+ try {
+ writer.write(key + "=" + value);
+ writer.newLine();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java
@@ -0,0 +1,129 @@
+/* -*- 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.util;
+
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Static helper class containing useful methods for manipulating IO objects.
+ */
+public class IOUtils {
+ private static final String LOGTAG = "GeckoIOUtils";
+
+ /**
+ * Represents the result of consuming an input stream, holding the returned data as well
+ * as the length of the data returned.
+ * The byte[] is not guaranteed to be trimmed to the size of the data acquired from the stream:
+ * hence the need for the length field. This strategy avoids the need to copy the data into a
+ * trimmed buffer after consumption.
+ */
+ public static class ConsumedInputStream {
+ public final int consumedLength;
+ // Only reassigned in getTruncatedData.
+ private byte[] consumedData;
+
+ public ConsumedInputStream(int consumedLength, byte[] consumedData) {
+ this.consumedLength = consumedLength;
+ this.consumedData = consumedData;
+ }
+
+ /**
+ * Get the data trimmed to the length of the actual payload read, caching the result.
+ */
+ public byte[] getTruncatedData() {
+ if (consumedData.length == consumedLength) {
+ return consumedData;
+ }
+
+ consumedData = truncateBytes(consumedData, consumedLength);
+ return consumedData;
+ }
+
+ public byte[] getData() {
+ return consumedData;
+ }
+ }
+
+ /**
+ * Fully read an InputStream into a byte array.
+ * @param iStream the InputStream to consume.
+ * @param bufferSize The initial size of the buffer to allocate. It will be grown as
+ * needed, but if the caller knows something about the InputStream then
+ * passing a good value here can improve performance.
+ */
+ public static ConsumedInputStream readFully(InputStream iStream, int bufferSize) {
+ // Allocate a buffer to hold the raw data downloaded.
+ byte[] buffer = new byte[bufferSize];
+
+ // The offset of the start of the buffer's free space.
+ int bPointer = 0;
+
+ // The quantity of bytes the last call to read yielded.
+ int lastRead = 0;
+ try {
+ // Fully read the data into the buffer.
+ while (lastRead != -1) {
+ // Read as many bytes as are currently available into the buffer.
+ lastRead = iStream.read(buffer, bPointer, buffer.length - bPointer);
+ bPointer += lastRead;
+
+ // If buffer has overflowed, double its size and carry on.
+ if (bPointer == buffer.length) {
+ bufferSize *= 2;
+ byte[] newBuffer = new byte[bufferSize];
+
+ // Copy the contents of the old buffer into the new buffer.
+ System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
+ buffer = newBuffer;
+ }
+ }
+
+ return new ConsumedInputStream(bPointer + 1, buffer);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error consuming input stream.", e);
+ } finally {
+ try {
+ iStream.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error closing input stream.", e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Truncate a given byte[] to a given length. Returns a new byte[] with the first length many
+ * bytes of the input.
+ */
+ public static byte[] truncateBytes(byte[] bytes, int length) {
+ byte[] newBytes = new byte[length];
+ System.arraycopy(bytes, 0, newBytes, 0, length);
+
+ return newBytes;
+ }
+
+ public static void safeStreamClose(Closeable stream) {
+ try {
+ if (stream != null)
+ stream.close();
+ } catch (IOException e) { }
+ }
+
+ public static void copy(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[4096];
+ int len;
+
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java
@@ -0,0 +1,45 @@
+/* -*- 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.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.speech.RecognizerIntent;
+
+public class InputOptionsUtils {
+ public static boolean supportsVoiceRecognizer(Context context, String prompt) {
+ final Intent intent = createVoiceRecognizerIntent(prompt);
+ return intent.resolveActivity(context.getPackageManager()) != null;
+ }
+
+ public static Intent createVoiceRecognizerIntent(String prompt) {
+ final Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+ intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
+ intent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
+ return intent;
+ }
+
+ public static boolean supportsIntent(Intent intent, Context context) {
+ return intent.resolveActivity(context.getPackageManager()) != null;
+ }
+
+ public static boolean supportsQrCodeReader(Context context) {
+ final Intent intent = createQRCodeReaderIntent();
+ return supportsIntent(intent, context);
+ }
+
+ public static Intent createQRCodeReaderIntent() {
+ // Bug 602818 enables QR code input if you have the particular app below installed in your device
+ final String appPackage = "com.google.zxing.client.android";
+
+ Intent intent = new Intent(appPackage + ".SCAN");
+ intent.setPackage(appPackage);
+ intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ return intent;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import org.mozilla.gecko.mozglue.SafeIntent;
+
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for Intents.
+ */
+public class IntentUtils {
+ public static final String ENV_VAR_IN_AUTOMATION = "MOZ_IN_AUTOMATION";
+
+ private static final String ENV_VAR_REGEX = "(.+)=(.*)";
+
+ private IntentUtils() {}
+
+ /**
+ * Returns a list of environment variables and their values. These are parsed from an Intent extra
+ * with the key -> value format:
+ * env# -> ENV_VAR=VALUE
+ *
+ * # in env# is expected to be increasing from 0.
+ *
+ * @return A Map of environment variable name to value, e.g. ENV_VAR -> VALUE
+ */
+ public static HashMap<String, String> getEnvVarMap(@NonNull final Intent unsafeIntent) {
+ // Optimization: get matcher for re-use. Pattern.matcher creates a new object every time so it'd be great
+ // to avoid the unnecessary allocation, particularly because we expect to be called on the startup path.
+ final Pattern envVarPattern = Pattern.compile(ENV_VAR_REGEX);
+ final Matcher matcher = envVarPattern.matcher(""); // argument does not matter here.
+
+ // This is expected to be an external intent so we should use SafeIntent to prevent crashing.
+ final SafeIntent safeIntent = new SafeIntent(unsafeIntent);
+ final HashMap<String, String> out = new HashMap<>();
+ int i = 0;
+ while (true) {
+ final String envKey = "env" + i;
+ i += 1;
+ if (!unsafeIntent.hasExtra(envKey)) {
+ break;
+ }
+
+ maybeAddEnvVarToEnvVarMap(out, safeIntent, envKey, matcher);
+ }
+ return out;
+ }
+
+ /**
+ * @param envVarMap the map to add the env var to
+ * @param intent the intent from which to extract the env var
+ * @param envKey the key at which the env var resides
+ * @param envVarMatcher a matcher initialized with the env var pattern to extract
+ */
+ private static void maybeAddEnvVarToEnvVarMap(@NonNull final HashMap<String, String> envVarMap,
+ @NonNull final SafeIntent intent, @NonNull final String envKey, @NonNull final Matcher envVarMatcher) {
+ final String envValue = intent.getStringExtra(envKey);
+ if (envValue == null) {
+ return; // nothing to do here!
+ }
+
+ envVarMatcher.reset(envValue);
+ if (envVarMatcher.matches()) {
+ final String envVarName = envVarMatcher.group(1);
+ final String envVarValue = envVarMatcher.group(2);
+ envVarMap.put(envVarName, envVarValue);
+ }
+ }
+
+ public static Bundle getBundleExtraSafe(final Intent intent, final String name) {
+ return new SafeIntent(intent).getBundleExtra(name);
+ }
+
+ public static String getStringExtraSafe(final Intent intent, final String name) {
+ return new SafeIntent(intent).getStringExtra(name);
+ }
+
+ public static boolean getBooleanExtraSafe(final Intent intent, final String name, final boolean defaultValue) {
+ return new SafeIntent(intent).getBooleanExtra(name, defaultValue);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java
@@ -0,0 +1,69 @@
+/* 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.util;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+public final class JSONUtils {
+ private static final String LOGTAG = "GeckoJSONUtils";
+
+ private JSONUtils() {}
+
+ public static UUID getUUID(String name, JSONObject json) {
+ String uuid = json.optString(name, null);
+ return (uuid != null) ? UUID.fromString(uuid) : null;
+ }
+
+ public static void putUUID(String name, UUID uuid, JSONObject json) {
+ String uuidString = uuid.toString();
+ try {
+ json.put(name, uuidString);
+ } catch (JSONException e) {
+ throw new IllegalArgumentException(name + "=" + uuidString, e);
+ }
+ }
+
+ public static JSONObject bundleToJSON(Bundle bundle) {
+ if (bundle == null || bundle.isEmpty()) {
+ return null;
+ }
+
+ JSONObject json = new JSONObject();
+ for (String key : bundle.keySet()) {
+ try {
+ json.put(key, bundle.get(key));
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Error building JSON response.", e);
+ }
+ }
+
+ return json;
+ }
+
+ // Handles conversions between a JSONArray and a Set<String>
+ public static Set<String> parseStringSet(JSONArray json) {
+ final Set<String> ret = new HashSet<String>();
+
+ for (int i = 0; i < json.length(); i++) {
+ try {
+ ret.add(json.getString(i));
+ } catch (JSONException ex) {
+ Log.i(LOGTAG, "Error parsing json", ex);
+ }
+ }
+
+ return ret;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java
@@ -0,0 +1,33 @@
+/* -*- 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.util;
+
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class MenuUtils {
+ /*
+ * This method looks for a menuitem and sets it's visible state, if
+ * it exists.
+ */
+ public static void safeSetVisible(Menu menu, int id, boolean visible) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) {
+ item.setVisible(visible);
+ }
+ }
+
+ /*
+ * This method looks for a menuitem and sets it's enabled state, if
+ * it exists.
+ */
+ public static void safeSetEnabled(Menu menu, int id, boolean enabled) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) {
+ item.setEnabled(enabled);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java
@@ -0,0 +1,23 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+@RobocopTarget
+public interface NativeEventListener {
+ /**
+ * Handles a message sent from Gecko.
+ *
+ * @param event The name of the event being sent.
+ * @param message The message data.
+ * @param callback The callback interface for this message. A callback is provided only if the
+ * originating Messaging.sendRequest call included a callback argument; otherwise,
+ * callback will be null. All listeners for a given event are given the same
+ * callback object, and exactly one listener must handle the callback.
+ */
+ void handleMessage(String event, NativeJSObject message, EventCallback callback);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java
@@ -0,0 +1,37 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * NativeJSContainer is a wrapper around the SpiderMonkey JSAPI to make it possible to
+ * access Javascript objects in Java.
+ *
+ * A container must only be used on the thread it is attached to. To use it on another
+ * thread, call {@link #clone()} to make a copy, and use the copy on the other thread.
+ * When a copy is first used, it becomes attached to the thread using it.
+ */
+@WrapForJNI
+public final class NativeJSContainer extends NativeJSObject
+{
+ private NativeJSContainer() {
+ }
+
+ /**
+ * Make a copy of this container for use by another thread. When the copy is first used,
+ * it becomes attached to the thread using it.
+ */
+ @Override
+ public native NativeJSContainer clone();
+
+ /**
+ * Dispose all associated native objects. Subsequent use of any objects derived from
+ * this container will throw a NullPointerException.
+ */
+ @Override
+ public native void disposeNative();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java
@@ -0,0 +1,533 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+import android.os.Bundle;
+
+/**
+ * NativeJSObject is a wrapper around the SpiderMonkey JSAPI to make it possible to
+ * access Javascript objects in Java.
+ */
+@WrapForJNI
+public class NativeJSObject extends JNIObject
+{
+ @SuppressWarnings("serial")
+ @JNITarget
+ public static final class InvalidPropertyException extends RuntimeException {
+ public InvalidPropertyException(final String msg) {
+ super(msg);
+ }
+ }
+
+ protected NativeJSObject() {
+ }
+
+ @Override
+ protected void disposeNative() {
+ // NativeJSObject is disposed as part of NativeJSContainer disposal.
+ }
+
+ /**
+ * Returns the value of a boolean property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean getBoolean(String name);
+
+ /**
+ * Returns the value of a boolean property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean optBoolean(String name, boolean fallback);
+
+ /**
+ * Returns the value of a boolean array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean[] getBooleanArray(String name);
+
+ /**
+ * Returns the value of a boolean array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean[] optBooleanArray(String name, boolean[] fallback);
+
+ /**
+ * Returns the value of an object property as a Bundle.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle getBundle(String name);
+
+ /**
+ * Returns the value of an object property as a Bundle.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle optBundle(String name, Bundle fallback);
+
+ /**
+ * Returns the value of an object array property as a Bundle array.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle[] getBundleArray(String name);
+
+ /**
+ * Returns the value of an object array property as a Bundle array.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle[] optBundleArray(String name, Bundle[] fallback);
+
+ /**
+ * Returns the value of a double property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double getDouble(String name);
+
+ /**
+ * Returns the value of a double property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double optDouble(String name, double fallback);
+
+ /**
+ * Returns the value of a double array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double[] getDoubleArray(String name);
+
+ /**
+ * Returns the value of a double array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double[] optDoubleArray(String name, double[] fallback);
+
+ /**
+ * Returns the value of an int property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int getInt(String name);
+
+ /**
+ * Returns the value of an int property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int optInt(String name, int fallback);
+
+ /**
+ * Returns the value of an int array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int[] getIntArray(String name);
+
+ /**
+ * Returns the value of an int array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int[] optIntArray(String name, int[] fallback);
+
+ /**
+ * Returns the value of an object property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject getObject(String name);
+
+ /**
+ * Returns the value of an object property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject optObject(String name, NativeJSObject fallback);
+
+ /**
+ * Returns the value of an object array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject[] getObjectArray(String name);
+
+ /**
+ * Returns the value of an object array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject[] optObjectArray(String name, NativeJSObject[] fallback);
+
+ /**
+ * Returns the value of a string property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String getString(String name);
+
+ /**
+ * Returns the value of a string property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String optString(String name, String fallback);
+
+ /**
+ * Returns the value of a string array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String[] getStringArray(String name);
+
+ /**
+ * Returns the value of a string array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String[] optStringArray(String name, String[] fallback);
+
+ /**
+ * Returns whether a property exists in this object
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean has(String name);
+
+ /**
+ * Returns the Bundle representation of this object.
+ *
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle toBundle();
+
+ /**
+ * Returns the JSON representation of this object.
+ *
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ @Override
+ public native String toString();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
@@ -0,0 +1,177 @@
+/* -*- 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.util;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.support.annotation.Nullable;
+import android.support.annotation.NonNull;
+import android.telephony.TelephonyManager;
+
+public class NetworkUtils {
+ /*
+ * Keep the below constants in sync with
+ * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum ConnectionSubType {
+ CELL_2G("2g"),
+ CELL_3G("3g"),
+ CELL_4G("4g"),
+ ETHERNET("ethernet"),
+ WIFI("wifi"),
+ WIMAX("wimax"),
+ UNKNOWN("unknown");
+
+ public final String value;
+ ConnectionSubType(String value) {
+ this.value = value;
+ }
+ }
+
+ /*
+ * Keep the below constants in sync with
+ * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum NetworkStatus {
+ UP("up"),
+ DOWN("down"),
+ UNKNOWN("unknown");
+
+ public final String value;
+
+ NetworkStatus(String value) {
+ this.value = value;
+ }
+ }
+
+ // Connection Type defined in Network Information API v3.
+ // See Bug 1270401 - current W3C Spec (Editor's Draft) is different, it also contains wimax, mixed, unknown.
+ // W3C spec: http://w3c.github.io/netinfo/#the-connectiontype-enum
+ public enum ConnectionType {
+ CELLULAR(0),
+ BLUETOOTH(1),
+ ETHERNET(2),
+ WIFI(3),
+ OTHER(4),
+ NONE(5);
+
+ public final int value;
+
+ ConnectionType(int value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Indicates whether network connectivity exists and it is possible to establish connections and pass data.
+ */
+ public static boolean isConnected(@NonNull Context context) {
+ return isConnected((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
+ }
+
+ public static boolean isConnected(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return false;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
+
+ /**
+ * For mobile connections, maps particular connection subtype to a general 2G, 3G, 4G bucket.
+ */
+ public static ConnectionSubType getConnectionSubType(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+
+ if (networkInfo == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionSubType.ETHERNET;
+ case ConnectivityManager.TYPE_MOBILE:
+ return getGenericMobileSubtype(networkInfo.getSubtype());
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionSubType.WIMAX;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionSubType.WIFI;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+
+ public static ConnectionType getConnectionType(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionType.NONE;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return ConnectionType.NONE;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_BLUETOOTH:
+ return ConnectionType.BLUETOOTH;
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionType.ETHERNET;
+ // Fallthrough, MOBILE and WIMAX both map to CELLULAR.
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionType.CELLULAR;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionType.WIFI;
+ default:
+ return ConnectionType.OTHER;
+ }
+ }
+
+ public static NetworkStatus getNetworkStatus(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return NetworkStatus.UNKNOWN;
+ }
+
+ if (isConnected(connectivityManager)) {
+ return NetworkStatus.UP;
+ }
+ return NetworkStatus.DOWN;
+ }
+
+ private static ConnectionSubType getGenericMobileSubtype(int subtype) {
+ switch (subtype) {
+ // 2G types: fallthrough 5x
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return ConnectionSubType.CELL_2G;
+ // 3G types: fallthrough 9x
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return ConnectionSubType.CELL_3G;
+ // 4G - just one type!
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return ConnectionSubType.CELL_4G;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java
@@ -0,0 +1,44 @@
+/* -*- 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.util;
+
+import android.util.LruCache;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * An LruCache that also supports a set of items that will never be evicted.
+ *
+ * Alas, LruCache is final, so we compose rather than inherit.
+ */
+public class NonEvictingLruCache<K, V> {
+ private final ConcurrentHashMap<K, V> permanent = new ConcurrentHashMap<K, V>();
+ private final LruCache<K, V> evictable;
+
+ public NonEvictingLruCache(final int evictableSize) {
+ evictable = new LruCache<K, V>(evictableSize);
+ }
+
+ public V get(K key) {
+ V val = permanent.get(key);
+ if (val == null) {
+ return evictable.get(key);
+ }
+ return val;
+ }
+
+ public void putWithoutEviction(K key, V value) {
+ permanent.put(key, value);
+ }
+
+ public void put(K key, V value) {
+ evictable.put(key, value);
+ }
+
+ public void evictAll() {
+ evictable.evictAll();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java
@@ -0,0 +1,70 @@
+/* -*- 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.util;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+
+public class PrefUtils {
+ private static final String LOGTAG = "GeckoPrefUtils";
+
+ // Cross version compatible way to get a string set from a pref
+ public static Set<String> getStringSet(final SharedPreferences prefs,
+ final String key,
+ final Set<String> defaultVal) {
+ if (!prefs.contains(key)) {
+ return defaultVal;
+ }
+
+ // If this is Android version >= 11, try to use a Set<String>.
+ try {
+ return prefs.getStringSet(key, new HashSet<String>());
+ } catch (ClassCastException ex) {
+ // A ClassCastException means we've upgraded from a pre-v11 Android to a new one
+ final Set<String> val = getFromJSON(prefs, key);
+ SharedPreferences.Editor edit = prefs.edit();
+ putStringSet(edit, key, val).apply();
+ return val;
+ }
+ }
+
+ private static Set<String> getFromJSON(SharedPreferences prefs, String key) {
+ try {
+ final String val = prefs.getString(key, "[]");
+ return JSONUtils.parseStringSet(new JSONArray(val));
+ } catch (JSONException ex) {
+ Log.i(LOGTAG, "Unable to parse JSON", ex);
+ }
+
+ return new HashSet<String>();
+ }
+
+ /**
+ * Cross version compatible way to save a set of strings.
+ * <p>
+ * This method <b>does not commit</b> any transaction. It is up to callers
+ * to commit.
+ *
+ * @param editor to write to.
+ * @param key to write.
+ * @param vals comprising string set.
+ * @return
+ */
+ public static SharedPreferences.Editor putStringSet(final SharedPreferences.Editor editor,
+ final String key,
+ final Set<String> vals) {
+ editor.putStringSet(key, vals);
+ return editor;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
@@ -0,0 +1,155 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// This code is based on AOSP /libcore/luni/src/main/java/java/net/ProxySelectorImpl.java
+
+package org.mozilla.gecko.util;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+
+public class ProxySelector {
+ public static URLConnection openConnectionWithProxy(URI uri) throws IOException {
+ java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
+ Proxy proxy = Proxy.NO_PROXY;
+ if (ps != null) {
+ List<Proxy> proxies = ps.select(uri);
+ if (proxies != null && !proxies.isEmpty()) {
+ proxy = proxies.get(0);
+ }
+ }
+
+ return uri.toURL().openConnection(proxy);
+ }
+
+ public ProxySelector() {
+ }
+
+ public Proxy select(String scheme, String host) {
+ int port = -1;
+ Proxy proxy = null;
+ String nonProxyHostsKey = null;
+ boolean httpProxyOkay = true;
+ if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ nonProxyHostsKey = "http.nonProxyHosts";
+ proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
+ proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("ftp".equalsIgnoreCase(scheme)) {
+ port = 80; // not 21 as you might guess
+ nonProxyHostsKey = "ftp.nonProxyHosts";
+ proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("socket".equalsIgnoreCase(scheme)) {
+ httpProxyOkay = false;
+ } else {
+ return Proxy.NO_PROXY;
+ }
+
+ if (nonProxyHostsKey != null
+ && isNonProxyHost(host, System.getProperty(nonProxyHostsKey))) {
+ return Proxy.NO_PROXY;
+ }
+
+ if (proxy != null) {
+ return proxy;
+ }
+
+ if (httpProxyOkay) {
+ proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
+ if (proxy != null) {
+ return proxy;
+ }
+ }
+
+ proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
+ if (proxy != null) {
+ return proxy;
+ }
+
+ return Proxy.NO_PROXY;
+ }
+
+ /**
+ * Returns the proxy identified by the {@code hostKey} system property, or
+ * null.
+ */
+ @Nullable
+ private Proxy lookupProxy(String hostKey, String portKey, Proxy.Type type, int defaultPort) {
+ final String host = System.getProperty(hostKey);
+ if (TextUtils.isEmpty(host)) {
+ return null;
+ }
+
+ final int port = getSystemPropertyInt(portKey, defaultPort);
+ if (port == -1) {
+ // Port can be -1. See bug 1270529.
+ return null;
+ }
+
+ return new Proxy(type, InetSocketAddress.createUnresolved(host, port));
+ }
+
+ private int getSystemPropertyInt(String key, int defaultValue) {
+ String string = System.getProperty(key);
+ if (string != null) {
+ try {
+ return Integer.parseInt(string);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns true if the {@code nonProxyHosts} system property pattern exists
+ * and matches {@code host}.
+ */
+ private boolean isNonProxyHost(String host, String nonProxyHosts) {
+ if (host == null || nonProxyHosts == null) {
+ return false;
+ }
+
+ // construct pattern
+ StringBuilder patternBuilder = new StringBuilder();
+ for (int i = 0; i < nonProxyHosts.length(); i++) {
+ char c = nonProxyHosts.charAt(i);
+ switch (c) {
+ case '.':
+ patternBuilder.append("\\.");
+ break;
+ case '*':
+ patternBuilder.append(".*");
+ break;
+ default:
+ patternBuilder.append(c);
+ }
+ }
+ // check whether the host is the nonProxyHosts.
+ String pattern = patternBuilder.toString();
+ return host.matches(pattern);
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java
@@ -0,0 +1,52 @@
+/* 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.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+
+/**
+ * {@code RawResource} provides API to load raw resources in different
+ * forms. For now, we only load them as strings. We're using raw resources
+ * as localizable 'assets' as opposed to a string that can be directly
+ * translatable e.g. JSON file vs string.
+ *
+ * This is just a utility class to avoid code duplication for the different
+ * cases where need to read such assets.
+ */
+public final class RawResource {
+ public static String getAsString(Context context, int id) throws IOException {
+ InputStreamReader reader = null;
+
+ try {
+ final Resources res = context.getResources();
+ final InputStream is = res.openRawResource(id);
+ if (is == null) {
+ return null;
+ }
+
+ reader = new InputStreamReader(is);
+
+ final char[] buffer = new char[1024];
+ final StringWriter s = new StringWriter();
+
+ int n;
+ while ((n = reader.read(buffer, 0, buffer.length)) != -1) {
+ s.write(buffer, 0, n);
+ }
+
+ return s.toString();
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+ }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
@@ -0,0 +1,278 @@
+/* -*- 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.util;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+public class StringUtils {
+ private static final String LOGTAG = "GeckoStringUtils";
+
+ private static final String FILTER_URL_PREFIX = "filter://";
+ private static final String USER_ENTERED_URL_PREFIX = "user-entered:";
+
+ /*
+ * This method tries to guess if the given string could be a search query or URL,
+ * and returns a previous result if there is ambiguity
+ *
+ * Search examples:
+ * foo
+ * foo bar.com
+ * foo http://bar.com
+ *
+ * URL examples
+ * foo.com
+ * foo.c
+ * :foo
+ * http://foo.com bar
+ *
+ * wasSearchQuery specifies whether text was a search query before the latest change
+ * in text. In ambiguous cases where the new text can be either a search or a URL,
+ * wasSearchQuery is returned
+ */
+ public static boolean isSearchQuery(String text, boolean wasSearchQuery) {
+ // We remove leading and trailing white spaces when decoding URLs
+ text = text.trim();
+ if (text.length() == 0)
+ return wasSearchQuery;
+
+ int colon = text.indexOf(':');
+ int dot = text.indexOf('.');
+ int space = text.indexOf(' ');
+
+ // If a space is found before any dot and colon, we assume this is a search query
+ if (space > -1 && (colon == -1 || space < colon) && (dot == -1 || space < dot)) {
+ return true;
+ }
+ // Otherwise, if a dot or a colon is found, we assume this is a URL
+ if (dot > -1 || colon > -1) {
+ return false;
+ }
+ // Otherwise, text is ambiguous, and we keep its status unchanged
+ return wasSearchQuery;
+ }
+
+ /**
+ * Strip the ref from a URL, if present
+ *
+ * @return The base URL, without the ref. The original String is returned if it has no ref,
+ * of if the input is malformed.
+ */
+ public static String stripRef(final String inputURL) {
+ if (inputURL == null) {
+ return null;
+ }
+
+ final int refIndex = inputURL.indexOf('#');
+
+ if (refIndex >= 0) {
+ return inputURL.substring(0, refIndex);
+ }
+
+ return inputURL;
+ }
+
+ public static class UrlFlags {
+ public static final int NONE = 0;
+ public static final int STRIP_HTTPS = 1;
+ }
+
+ public static String stripScheme(String url) {
+ return stripScheme(url, UrlFlags.NONE);
+ }
+
+ public static String stripScheme(String url, int flags) {
+ if (url == null) {
+ return url;
+ }
+
+ int start = 0;
+ int end = url.length();
+
+ if (url.startsWith("http://")) {
+ start = 7;
+ } else if (url.startsWith("https://") && flags == UrlFlags.STRIP_HTTPS) {
+ start = 8;
+ }
+
+ if (url.endsWith("/")) {
+ end--;
+ }
+
+ return url.substring(start, end);
+ }
+
+ public static boolean isHttpOrHttps(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return false;
+ }
+
+ return url.startsWith("http://") || url.startsWith("https://");
+ }
+
+ public static String stripCommonSubdomains(String host) {
+ if (host == null) {
+ return host;
+ }
+
+ // In contrast to desktop, we also strip mobile subdomains,
+ // since its unlikely users are intentionally typing them
+ int start = 0;
+
+ if (host.startsWith("www.")) {
+ start = 4;
+ } else if (host.startsWith("mobile.")) {
+ start = 7;
+ } else if (host.startsWith("m.")) {
+ start = 2;
+ }
+
+ return host.substring(start);
+ }
+
+ /**
+ * Searches the url query string for the first value with the given key.
+ */
+ public static String getQueryParameter(String url, String desiredKey) {
+ if (TextUtils.isEmpty(url) || TextUtils.isEmpty(desiredKey)) {
+ return null;
+ }
+
+ final String[] urlParts = url.split("\\?");
+ if (urlParts.length < 2) {
+ return null;
+ }
+
+ final String query = urlParts[1];
+ for (final String param : query.split("&")) {
+ final String pair[] = param.split("=");
+ final String key = Uri.decode(pair[0]);
+
+ // Key is empty or does not match the key we're looking for, discard
+ if (TextUtils.isEmpty(key) || !key.equals(desiredKey)) {
+ continue;
+ }
+ // No value associated with key, discard
+ if (pair.length < 2) {
+ continue;
+ }
+ final String value = Uri.decode(pair[1]);
+ if (TextUtils.isEmpty(value)) {
+ return null;
+ }
+ return value;
+ }
+
+ return null;
+ }
+
+ public static boolean isFilterUrl(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return false;
+ }
+
+ return url.startsWith(FILTER_URL_PREFIX);
+ }
+
+ public static String getFilterFromUrl(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return null;
+ }
+
+ return url.substring(FILTER_URL_PREFIX.length());
+ }
+
+ public static boolean isShareableUrl(final String url) {
+ final String scheme = Uri.parse(url).getScheme();
+ return !("about".equals(scheme) || "chrome".equals(scheme) ||
+ "file".equals(scheme) || "resource".equals(scheme));
+ }
+
+ public static boolean isUserEnteredUrl(String url) {
+ return (url != null && url.startsWith(USER_ENTERED_URL_PREFIX));
+ }
+
+ /**
+ * Given a url with a user-entered scheme, extract the
+ * scheme-specific component. For e.g, given "user-entered://www.google.com",
+ * this method returns "//www.google.com". If the passed url
+ * does not have a user-entered scheme, the same url will be returned.
+ *
+ * @param url to be decoded
+ * @return url component entered by user
+ */
+ public static String decodeUserEnteredUrl(String url) {
+ Uri uri = Uri.parse(url);
+ if ("user-entered".equals(uri.getScheme())) {
+ return uri.getSchemeSpecificPart();
+ }
+ return url;
+ }
+
+ public static String encodeUserEnteredUrl(String url) {
+ return Uri.fromParts("user-entered", url, null).toString();
+ }
+
+ /**
+ * Compatibility layer for API < 11.
+ *
+ * Returns a set of the unique names of all query parameters. Iterating
+ * over the set will return the names in order of their first occurrence.
+ *
+ * @param uri
+ * @throws UnsupportedOperationException if this isn't a hierarchical URI
+ *
+ * @return a set of decoded names
+ */
+ public static Set<String> getQueryParameterNames(Uri uri) {
+ if (Versions.feature11Plus) {
+ return uri.getQueryParameterNames();
+ }
+
+ // Logic below copied from Uri.java included with Android 5.0.0.
+ if (uri.isOpaque()) {
+ throw new UnsupportedOperationException("This isn't a hierarchical URI.");
+ }
+
+ String query = uri.getEncodedQuery();
+ if (query == null) {
+ return Collections.emptySet();
+ }
+
+ Set<String> names = new LinkedHashSet<String>();
+ int start = 0;
+ do {
+ int next = query.indexOf('&', start);
+ int end = (next == -1) ? query.length() : next;
+
+ int separator = query.indexOf('=', start);
+ if (separator > end || separator == -1) {
+ separator = end;
+ }
+
+ String name = query.substring(start, separator);
+ names.add(Uri.decode(name));
+
+ // Move start to end of name.
+ start = end + 1;
+ } while (start < query.length());
+
+ return Collections.unmodifiableSet(names);
+ }
+
+ public static String safeSubstring(@NonNull final String str, final int start, final int end) {
+ return str.substring(
+ Math.max(0, start),
+ Math.min(end, str.length()));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
@@ -0,0 +1,247 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import java.util.Map;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.Log;
+
+public final class ThreadUtils {
+ private static final String LOGTAG = "ThreadUtils";
+
+ /**
+ * Controls the action taken when a method like
+ * {@link ThreadUtils#assertOnUiThread(AssertBehavior)} detects a problem.
+ */
+ public static enum AssertBehavior {
+ NONE,
+ THROW,
+ }
+
+ private static final Thread sUiThread = Looper.getMainLooper().getThread();
+ private static final Handler sUiHandler = new Handler(Looper.getMainLooper());
+
+ private static volatile Thread sBackgroundThread;
+
+ // Referenced directly from GeckoAppShell in highly performance-sensitive code (The extra
+ // function call of the getter was harming performance. (Bug 897123))
+ // Once Bug 709230 is resolved we should reconsider this as ProGuard should be able to optimise
+ // this out at compile time.
+ public static Handler sGeckoHandler;
+ public static volatile Thread sGeckoThread;
+
+ // Delayed Runnable that resets the Gecko thread priority.
+ private static final Runnable sPriorityResetRunnable = new Runnable() {
+ @Override
+ public void run() {
+ resetGeckoPriority();
+ }
+ };
+
+ private static boolean sIsGeckoPriorityReduced;
+
+ @SuppressWarnings("serial")
+ public static class UiThreadBlockedException extends RuntimeException {
+ public UiThreadBlockedException() {
+ super();
+ }
+
+ public UiThreadBlockedException(String msg) {
+ super(msg);
+ }
+
+ public UiThreadBlockedException(String msg, Throwable e) {
+ super(msg, e);
+ }
+
+ public UiThreadBlockedException(Throwable e) {
+ super(e);
+ }
+ }
+
+ public static void dumpAllStackTraces() {
+ Log.w(LOGTAG, "Dumping ALL the threads!");
+ Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces();
+ for (Thread t : allStacks.keySet()) {
+ Log.w(LOGTAG, t.toString());
+ for (StackTraceElement ste : allStacks.get(t)) {
+ Log.w(LOGTAG, ste.toString());
+ }
+ Log.w(LOGTAG, "----");
+ }
+ }
+
+ public static void setBackgroundThread(Thread thread) {
+ sBackgroundThread = thread;
+ }
+
+ public static Thread getUiThread() {
+ return sUiThread;
+ }
+
+ public static Handler getUiHandler() {
+ return sUiHandler;
+ }
+
+ public static void postToUiThread(Runnable runnable) {
+ sUiHandler.post(runnable);
+ }
+
+ public static void postDelayedToUiThread(Runnable runnable, long timeout) {
+ sUiHandler.postDelayed(runnable, timeout);
+ }
+
+ public static void removeCallbacksFromUiThread(Runnable runnable) {
+ sUiHandler.removeCallbacks(runnable);
+ }
+
+ public static Thread getBackgroundThread() {
+ return sBackgroundThread;
+ }
+
+ public static Handler getBackgroundHandler() {
+ return GeckoBackgroundThread.getHandler();
+ }
+
+ public static void postToBackgroundThread(Runnable runnable) {
+ GeckoBackgroundThread.post(runnable);
+ }
+
+ public static void assertOnUiThread(final AssertBehavior assertBehavior) {
+ assertOnThread(getUiThread(), assertBehavior);
+ }
+
+ public static void assertOnUiThread() {
+ assertOnThread(getUiThread(), AssertBehavior.THROW);
+ }
+
+ public static void assertNotOnUiThread() {
+ assertNotOnThread(getUiThread(), AssertBehavior.THROW);
+ }
+
+ @RobocopTarget
+ public static void assertOnGeckoThread() {
+ assertOnThread(sGeckoThread, AssertBehavior.THROW);
+ }
+
+ public static void assertNotOnGeckoThread() {
+ if (sGeckoThread == null) {
+ // Cannot be on Gecko thread if Gecko thread is not live yet.
+ return;
+ }
+ assertNotOnThread(sGeckoThread, AssertBehavior.THROW);
+ }
+
+ public static void assertOnBackgroundThread() {
+ assertOnThread(getBackgroundThread(), AssertBehavior.THROW);
+ }
+
+ public static void assertOnThread(final Thread expectedThread) {
+ assertOnThread(expectedThread, AssertBehavior.THROW);
+ }
+
+ public static void assertOnThread(final Thread expectedThread, AssertBehavior behavior) {
+ assertOnThreadComparison(expectedThread, behavior, true);
+ }
+
+ public static void assertNotOnThread(final Thread expectedThread, AssertBehavior behavior) {
+ assertOnThreadComparison(expectedThread, behavior, false);
+ }
+
+ private static void assertOnThreadComparison(final Thread expectedThread, AssertBehavior behavior, boolean expected) {
+ final Thread currentThread = Thread.currentThread();
+ final long currentThreadId = currentThread.getId();
+ final long expectedThreadId = expectedThread.getId();
+
+ if ((currentThreadId == expectedThreadId) == expected) {
+ return;
+ }
+
+ final String message;
+ if (expected) {
+ message = "Expected thread " + expectedThreadId +
+ " (\"" + expectedThread.getName() + "\"), but running on thread " +
+ currentThreadId + " (\"" + currentThread.getName() + "\")";
+ } else {
+ message = "Expected anything but " + expectedThreadId +
+ " (\"" + expectedThread.getName() + "\"), but running there.";
+ }
+
+ final IllegalThreadStateException e = new IllegalThreadStateException(message);
+
+ switch (behavior) {
+ case THROW:
+ throw e;
+ default:
+ Log.e(LOGTAG, "Method called on wrong thread!", e);
+ }
+ }
+
+ public static boolean isOnGeckoThread() {
+ if (sGeckoThread != null) {
+ return isOnThread(sGeckoThread);
+ }
+ return false;
+ }
+
+ public static boolean isOnUiThread() {
+ return isOnThread(getUiThread());
+ }
+
+ @RobocopTarget
+ public static boolean isOnBackgroundThread() {
+ if (sBackgroundThread == null) {
+ return false;
+ }
+
+ return isOnThread(sBackgroundThread);
+ }
+
+ @RobocopTarget
+ public static boolean isOnThread(Thread thread) {
+ return (Thread.currentThread().getId() == thread.getId());
+ }
+
+ /**
+ * Reduces the priority of the Gecko thread, allowing other operations
+ * (such as those related to the UI and database) to take precedence.
+ *
+ * Note that there are no guards in place to prevent multiple calls
+ * to this method from conflicting with each other.
+ *
+ * @param timeout Timeout in ms after which the priority will be reset
+ */
+ public static void reduceGeckoPriority(long timeout) {
+ if (Runtime.getRuntime().availableProcessors() > 1) {
+ // Don't reduce priority for multicore devices. We use availableProcessors()
+ // for its fast performance. It may give false negatives (i.e. multicore
+ // detected as single-core), but we can tolerate this behavior.
+ return;
+ }
+ if (!sIsGeckoPriorityReduced && sGeckoThread != null) {
+ sIsGeckoPriorityReduced = true;
+ sGeckoThread.setPriority(Thread.MIN_PRIORITY);
+ getUiHandler().postDelayed(sPriorityResetRunnable, timeout);
+ }
+ }
+
+ /**
+ * Resets the priority of a thread whose priority has been reduced
+ * by reduceGeckoPriority.
+ */
+ public static void resetGeckoPriority() {
+ if (sIsGeckoPriorityReduced) {
+ sIsGeckoPriorityReduced = false;
+ sGeckoThread.setPriority(Thread.NORM_PRIORITY);
+ getUiHandler().removeCallbacks(sPriorityResetRunnable);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java
@@ -0,0 +1,121 @@
+/* -*- 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.util;
+
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Executes a background task and publishes the result on the UI thread.
+ *
+ * The standard {@link android.os.AsyncTask} only runs onPostExecute on the
+ * thread it is constructed on, so this is a convenience class for creating
+ * tasks off the UI thread.
+ *
+ * We use generics differently to Android's AsyncTask.
+ * Android uses a "Params" type parameter to represent the type of all the parameters to this task.
+ * It then uses arguments of type Params... to permit arbitrarily-many of these to be passed
+ * fluently.
+ *
+ * Unfortunately, since Java does not support generic array types (and since varargs desugars to a
+ * single array parameter) that behaviour exposes a hole in the type system. See:
+ * http://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html#vulnerabilities
+ *
+ * Instead, we equivalently have a single type parameter "Param". A UiAsyncTask may take exactly one
+ * parameter of type Param. Since Param can be an array type, this no more restrictive than the
+ * other approach, it just provides additional type safety.
+ */
+public abstract class UIAsyncTask<Param, Result> {
+ /**
+ * Provide a convenient API for parameter-free UiAsyncTasks by wrapping parameter-taking methods
+ * from UiAsyncTask in parameterless equivalents.
+ */
+ public static abstract class WithoutParams<InnerResult> extends UIAsyncTask<Void, InnerResult> {
+ public WithoutParams(Handler backgroundThreadHandler) {
+ super(backgroundThreadHandler);
+ }
+
+ public void execute() {
+ execute(null);
+ }
+
+ @Override
+ protected InnerResult doInBackground(Void unused) {
+ return doInBackground();
+ }
+
+ protected abstract InnerResult doInBackground();
+ }
+
+ final Handler mBackgroundThreadHandler;
+ private volatile boolean mCancelled;
+ private static Handler sHandler;
+
+ /**
+ * Creates a new asynchronous task.
+ *
+ * @param backgroundThreadHandler the handler to execute the background task on
+ */
+ public UIAsyncTask(Handler backgroundThreadHandler) {
+ mBackgroundThreadHandler = backgroundThreadHandler;
+ }
+
+ private static synchronized Handler getUiHandler() {
+ if (sHandler == null) {
+ sHandler = new Handler(Looper.getMainLooper());
+ }
+
+ return sHandler;
+ }
+
+ private final class BackgroundTaskRunnable implements Runnable {
+ private final Param mParam;
+
+ public BackgroundTaskRunnable(Param param) {
+ mParam = param;
+ }
+
+ @Override
+ public void run() {
+ final Result result = doInBackground(mParam);
+
+ getUiHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (mCancelled) {
+ onCancelled();
+ } else {
+ onPostExecute(result);
+ }
+ }
+ });
+ }
+ }
+
+ protected void execute(final Param param) {
+ getUiHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ onPreExecute();
+ mBackgroundThreadHandler.post(new BackgroundTaskRunnable(param));
+ }
+ });
+ }
+
+ public final boolean cancel() {
+ mCancelled = true;
+ return mCancelled;
+ }
+
+ public final boolean isCancelled() {
+ return mCancelled;
+ }
+
+ protected void onPreExecute() { }
+ protected void onPostExecute(Result result) { }
+ protected void onCancelled() { }
+ protected abstract Result doInBackground(Param param);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java
@@ -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.util;
+
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for UUIDs.
+ */
+public class UUIDUtil {
+ private UUIDUtil() {}
+
+ public static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
+ public static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java
@@ -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.util;
+
+import android.os.Handler;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A Handler to help prevent memory leaks when using Handlers as inner classes.
+ *
+ * To use, extend the Handler, if it's an inner class, make it static,
+ * and reference `this` via the associated WeakReference.
+ *
+ * For additional context, see the "HandlerLeak" android lint item and this post by Romain Guy:
+ * https://groups.google.com/forum/#!msg/android-developers/1aPZXZG6kWk/lIYDavGYn5UJ
+ */
+public class WeakReferenceHandler<T> extends Handler {
+ public final WeakReference<T> mTarget;
+
+ public WeakReferenceHandler(final T that) {
+ super();
+ mTarget = new WeakReference<>(that);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WebActivityMapper.java
@@ -0,0 +1,221 @@
+/* -*- 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.util;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class WebActivityMapper {
+ private static final String LOGTAG = "Gecko";
+
+ private static final Map<String, WebActivityMapping> activityMap = new HashMap<String, WebActivityMapping>();
+ static {
+ activityMap.put("dial", new DialMapping());
+ activityMap.put("open", new OpenMapping());
+ activityMap.put("pick", new PickMapping());
+ activityMap.put("send", new SendMapping());
+ activityMap.put("view", new ViewMapping());
+ activityMap.put("record", new RecordMapping());
+ };
+
+ private static abstract class WebActivityMapping {
+ protected JSONObject mData;
+
+ public void setData(JSONObject data) {
+ mData = data;
+ }
+
+ // Cannot return null
+ public abstract String getAction();
+
+ public String getMime() throws JSONException {
+ return null;
+ }
+
+ public String getUri() throws JSONException {
+ return null;
+ }
+
+ public void putExtras(Intent intent) throws JSONException {}
+ }
+
+ /**
+ * Provides useful defaults for mime type and uri.
+ */
+ private static abstract class BaseMapping extends WebActivityMapping {
+ /**
+ * If 'type' is present in data object, uses the value as the MIME type.
+ */
+ @Override
+ public String getMime() throws JSONException {
+ return mData.optString("type", null);
+ }
+
+ /**
+ * If 'uri' or 'url' is present in data object, uses the respective value as the Uri.
+ */
+ @Override
+ public String getUri() throws JSONException {
+ // Will return uri or url if present.
+ String uri = mData.optString("uri", null);
+ return uri != null ? uri : mData.optString("url", null);
+ }
+ }
+
+ public static Intent getIntentForWebActivity(JSONObject message) throws JSONException {
+ final String name = message.getString("name").toLowerCase();
+ final JSONObject data = message.getJSONObject("data");
+
+ Log.w(LOGTAG, "Activity is: " + name);
+ final WebActivityMapping mapping = activityMap.get(name);
+ if (mapping == null) {
+ Log.w(LOGTAG, "No mapping found!");
+ return null;
+ }
+
+ mapping.setData(data);
+
+ final Intent intent = new Intent(mapping.getAction());
+
+ final String mime = mapping.getMime();
+ if (!TextUtils.isEmpty(mime)) {
+ intent.setType(mime);
+ }
+
+ final String uri = mapping.getUri();
+ if (!TextUtils.isEmpty(uri)) {
+ intent.setData(Uri.parse(uri));
+ }
+
+ mapping.putExtras(intent);
+
+ return intent;
+ }
+
+ private static class DialMapping extends WebActivityMapping {
+ @Override
+ public String getAction() {
+ return Intent.ACTION_DIAL;
+ }
+
+ @Override
+ public String getUri() throws JSONException {
+ return "tel:" + mData.getString("number");
+ }
+ }
+
+ private static class OpenMapping extends BaseMapping {
+ @Override
+ public String getAction() {
+ return Intent.ACTION_VIEW;
+ }
+ }
+
+ private static class PickMapping extends BaseMapping {
+ @Override
+ public String getAction() {
+ return Intent.ACTION_GET_CONTENT;
+ }
+
+ @Override
+ public String getMime() throws JSONException {
+ // bug 1007112 - pick action needs a mimetype to work
+ String mime = mData.optString("type", null);
+ return !TextUtils.isEmpty(mime) ? mime : "*/*";
+ }
+ }
+
+ private static class SendMapping extends BaseMapping {
+ @Override
+ public String getAction() {
+ return Intent.ACTION_SEND;
+ }
+
+ @Override
+ public void putExtras(Intent intent) throws JSONException {
+ optPutExtra("text", Intent.EXTRA_TEXT, intent);
+ optPutExtra("html_text", Intent.EXTRA_HTML_TEXT, intent);
+ optPutExtra("stream", Intent.EXTRA_STREAM, intent);
+ }
+
+ private void optPutExtra(String key, String extraName, Intent intent) {
+ final String extraValue = mData.optString(key);
+ if (!TextUtils.isEmpty(extraValue)) {
+ intent.putExtra(extraName, extraValue);
+ }
+ }
+ }
+
+ private static class ViewMapping extends BaseMapping {
+ @Override
+ public String getAction() {
+ return Intent.ACTION_VIEW;
+ }
+
+ @Override
+ public String getMime() {
+ // MozActivity adds a type 'url' here, we don't want to set the MIME to 'url'.
+ String type = mData.optString("type", null);
+ if ("url".equals(type) || "uri".equals(type)) {
+ return null;
+ } else {
+ return type;
+ }
+ }
+ }
+
+ private static class RecordMapping extends WebActivityMapping {
+ @Override
+ public String getAction() {
+ String type = mData.optString("type", null);
+ if ("photos".equals(type)) {
+ return "android.media.action.IMAGE_CAPTURE";
+ } else if ("videos".equals(type)) {
+ return "android.media.action.VIDEO_CAPTURE";
+ }
+ return null;
+ }
+
+ // Add an extra to specify where to save the picture/video.
+ @Override
+ public void putExtras(Intent intent) {
+ final String action = getAction();
+
+ final String dirType = action == "android.media.action.IMAGE_CAPTURE"
+ ? Environment.DIRECTORY_PICTURES
+ : Environment.DIRECTORY_MOVIES;
+
+ final String ext = action == "android.media.action.IMAGE_CAPTURE"
+ ? ".jpg"
+ : ".mp4";
+
+ File destDir = Environment.getExternalStoragePublicDirectory(dirType);
+
+ try {
+ File dest = File.createTempFile(
+ "capture", /* prefix */
+ ext, /* suffix */
+ destDir /* directory */
+ );
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(dest));
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Failed to add extra for " + action + " : " + e);
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java
@@ -0,0 +1,62 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+
+import java.lang.reflect.Method;
+
+public class WindowUtils {
+ private static final String LOGTAG = "Gecko" + WindowUtils.class.getSimpleName();
+
+ private WindowUtils() { /* To prevent instantiation */ }
+
+ /**
+ * Returns the best-guess physical device dimensions, including the system status bars. Note
+ * that DisplayMetrics.height/widthPixels does not include the system bars.
+ *
+ * via http://stackoverflow.com/a/23861333
+ *
+ * @param context the calling Activity's Context
+ * @return The number of pixels of the device's largest dimension, ignoring software status bars
+ */
+ public static int getLargestDimension(final Context context) {
+ final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+
+ if (Versions.feature17Plus) {
+ final DisplayMetrics realMetrics = new DisplayMetrics();
+ display.getRealMetrics(realMetrics);
+ return Math.max(realMetrics.widthPixels, realMetrics.heightPixels);
+
+ } else if (Versions.feature14Plus) {
+ int tempWidth;
+ int tempHeight;
+ try {
+ final Method getRawH = Display.class.getMethod("getRawHeight");
+ final Method getRawW = Display.class.getMethod("getRawWidth");
+ tempWidth = (Integer) getRawW.invoke(display);
+ tempHeight = (Integer) getRawH.invoke(display);
+ } catch (Exception e) {
+ // This is the best we can do.
+ tempWidth = display.getWidth();
+ tempHeight = display.getHeight();
+ Log.w(LOGTAG, "Couldn't use reflection to get the real display metrics.");
+ }
+
+ return Math.max(tempWidth, tempHeight);
+
+ } else {
+ // This should be close, as lower API devices should not have window navigation bars.
+ return Math.max(display.getWidth(), display.getHeight());
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2012 Roman Nurik
+ *
+ * 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.widget;
+
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.RecyclerListener;
+import android.widget.ListView;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.view.ViewPropertyAnimator;
+
+import org.mozilla.gecko.R;
+
+/**
+ * This code is based off of Jake Wharton's NOA port (https://github.com/JakeWharton/SwipeToDismissNOA)
+ * of Roman Nurik's SwipeToDismiss library. It has been modified for better support with async
+ * adapters.
+ *
+ * A {@link android.view.View.OnTouchListener} that makes the list items in a {@link ListView}
+ * dismissable. {@link ListView} is given special treatment because by default it handles touches
+ * for its list items... i.e. it's in charge of drawing the pressed state (the list selector),
+ * handling list item clicks, etc.
+ *
+ * <p>After creating the listener, the caller should also call
+ * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing
+ * in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is
+ * already assigned, the caller should still pass scroll changes through to this listener. This will
+ * ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view
+ * scrolling.</p>
+ *
+ * <p>Example usage:</p>
+ *
+ * <pre>
+ * SwipeDismissListViewTouchListener touchListener =
+ * new SwipeDismissListViewTouchListener(
+ * listView,
+ * new SwipeDismissListViewTouchListener.OnDismissCallback() {
+ * public void onDismiss(ListView listView, int[] reverseSortedPositions) {
+ * for (int position : reverseSortedPositions) {
+ * adapter.remove(adapter.getItem(position));
+ * }
+ * adapter.notifyDataSetChanged();
+ * }
+ * });
+ * listView.setOnTouchListener(touchListener);
+ * listView.setOnScrollListener(touchListener.makeScrollListener());
+ * </pre>
+ *
+ * <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
+ * see {@link SwipeDismissTouchListener}.</p>
+ *
+ * @see SwipeDismissTouchListener
+ */
+public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
+ // Cached ViewConfiguration and system-wide constant values
+ private final int mSlop;
+ private final int mMinFlingVelocity;
+ private final int mMaxFlingVelocity;
+ private final long mAnimationTime;
+
+ // Fixed properties
+ private final ListView mListView;
+ private final OnDismissCallback mCallback;
+ private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
+
+ // Transient properties
+ private float mDownX;
+ private boolean mSwiping;
+ private VelocityTracker mVelocityTracker;
+ private int mDownPosition;
+ private View mDownView;
+ private boolean mPaused;
+ private boolean mDismissing;
+
+ /**
+ * The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
+ * about a successful dismissal of a list item.
+ */
+ public interface OnDismissCallback {
+ /**
+ * Called when the user has indicated they she would like to dismiss one or more list item
+ * positions.
+ *
+ * @param listView The originating {@link ListView}.
+ * @param position The position being dismissed.
+ */
+ void onDismiss(ListView listView, int position);
+ }
+
+ /**
+ * Constructs a new swipe-to-dismiss touch listener for the given list view.
+ *
+ * @param listView The list view whose items should be dismissable.
+ * @param callback The callback to trigger when the user has indicated that she would like to
+ * dismiss one or more list items.
+ */
+ public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
+ ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
+ mSlop = vc.getScaledTouchSlop();
+ mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+ mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
+ mAnimationTime = listView.getContext().getResources().getInteger(
+ android.R.integer.config_shortAnimTime);
+ mListView = listView;
+ mCallback = callback;
+ }
+
+ /**
+ * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
+ *
+ * @param enabled Whether or not to watch for gestures.
+ */
+ public void setEnabled(boolean enabled) {
+ mPaused = !enabled;
+ }
+
+ /**
+ * Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the
+ * {@link ListView} using
+ * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
+ * If a scroll listener is already assigned, the caller should still pass scroll changes
+ * through to this listener. This will ensure that this
+ * {@link SwipeDismissListViewTouchListener} is paused during list view scrolling.</p>
+ *
+ * @see {@link SwipeDismissListViewTouchListener}
+ */
+ public AbsListView.OnScrollListener makeScrollListener() {
+ return new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView absListView, int scrollState) {
+ setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ }
+
+ @Override
+ public void onScroll(AbsListView absListView, int i, int i1, int i2) {
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link android.widget.AbsListView.RecyclerListener} to be added to the
+ * {@link ListView} using {@link ListView#setRecyclerListener(RecyclerListener)}.
+ */
+ public AbsListView.RecyclerListener makeRecyclerListener() {
+ return new AbsListView.RecyclerListener() {
+ @Override
+ public void onMovedToScrapHeap(View view) {
+ final Object tag = view.getTag(R.id.original_height);
+
+ // To reset the view to the correct height after its animation, the view's height
+ // is stored in its tag. Reset the view here.
+ if (tag instanceof Integer) {
+ view.setAlpha(1f);
+ view.setTranslationX(0);
+ final ViewGroup.LayoutParams lp = view.getLayoutParams();
+ lp.height = (int) tag;
+ view.setLayoutParams(lp);
+ view.setTag(R.id.original_height, null);
+ }
+ }
+ };
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ if (mViewWidth < 2) {
+ mViewWidth = mListView.getWidth();
+ }
+
+ switch (motionEvent.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ if (mPaused) {
+ return false;
+ }
+
+ if (mDismissing) {
+ return true;
+ }
+
+ // TODO: ensure this is a finger, and set a flag
+
+ // Find the child view that was touched (perform a hit test)
+ Rect rect = new Rect();
+ int childCount = mListView.getChildCount();
+ int[] listViewCoords = new int[2];
+ mListView.getLocationOnScreen(listViewCoords);
+ int x = (int) motionEvent.getRawX() - listViewCoords[0];
+ int y = (int) motionEvent.getRawY() - listViewCoords[1];
+ View child;
+ for (int i = 0; i < childCount; i++) {
+ child = mListView.getChildAt(i);
+ child.getHitRect(rect);
+ if (rect.contains(x, y)) {
+ mDownView = child;
+ break;
+ }
+ }
+
+ if (mDownView != null) {
+ mDownX = motionEvent.getRawX();
+ mDownPosition = mListView.getPositionForView(mDownView);
+
+ mVelocityTracker = VelocityTracker.obtain();
+ mVelocityTracker.addMovement(motionEvent);
+ }
+ view.onTouchEvent(motionEvent);
+ return true;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ if (mVelocityTracker == null) {
+ break;
+ }
+
+ float deltaX = motionEvent.getRawX() - mDownX;
+ mVelocityTracker.addMovement(motionEvent);
+ mVelocityTracker.computeCurrentVelocity(1000);
+ float velocityX = Math.abs(mVelocityTracker.getXVelocity());
+ float velocityY = Math.abs(mVelocityTracker.getYVelocity());
+ boolean dismiss = false;
+ boolean dismissRight = false;
+ if (Math.abs(deltaX) > mViewWidth / 2) {
+ dismiss = true;
+ dismissRight = deltaX > 0;
+ } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
+ && velocityY < velocityX) {
+ dismiss = true;
+ dismissRight = mVelocityTracker.getXVelocity() > 0;
+ }
+ if (dismiss) {
+ // dismiss
+ mDismissing = true;
+ final View downView = mDownView; // mDownView gets null'd before animation ends
+ final int downPosition = mDownPosition;
+ mDownView.animate()
+ .translationX(dismissRight ? mViewWidth : -mViewWidth)
+ .alpha(0)
+ .setDuration(mAnimationTime)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ performDismiss(downView, downPosition);
+ }
+ });
+ } else {
+ // cancel
+ mDownView.animate()
+ .translationX(0)
+ .alpha(1)
+ .setDuration(mAnimationTime)
+ .setListener(null);
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ mDownX = 0;
+ mDownView = null;
+ mDownPosition = ListView.INVALID_POSITION;
+ mSwiping = false;
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ if (mVelocityTracker == null || mPaused) {
+ break;
+ }
+
+ mVelocityTracker.addMovement(motionEvent);
+ float deltaX = motionEvent.getRawX() - mDownX;
+ if (Math.abs(deltaX) > mSlop) {
+ mSwiping = true;
+ mListView.requestDisallowInterceptTouchEvent(true);
+
+ // Cancel ListView's touch (un-highlighting the item)
+ MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
+ cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
+ (motionEvent.getActionIndex()
+ << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
+ mListView.onTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+
+ if (mSwiping) {
+ mDownView.setTranslationX(deltaX);
+ mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth)));
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Animate the dismissed list item to zero-height and fire the dismiss callback when it finishes.
+ *
+ * @param dismissView ListView item to dismiss
+ * @param dismissPosition Position of dismissed item
+ */
+ private void performDismiss(final View dismissView, final int dismissPosition) {
+ final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
+ final int originalHeight = lp.height;
+
+ ValueAnimator animator = ValueAnimator.ofInt(dismissView.getHeight(), 1).setDuration(mAnimationTime);
+
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Since the view is still a part of the ListView, we can't reset the animated
+ // properties yet; otherwise, the view would briefly reappear. Store the original
+ // height in the view's tag to flag it for the recycler. This is racy since the user
+ // could scroll the dismissed view off the screen, then back on the screen, before
+ // it's removed from the adapter, causing the dismissed view to briefly reappear.
+ dismissView.setTag(R.id.original_height, originalHeight);
+
+ mCallback.onDismiss(mListView, dismissPosition);
+ mDismissing = false;
+ }
+ });
+
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ lp.height = (Integer) valueAnimator.getAnimatedValue();
+ dismissView.setLayoutParams(lp);
+ }
+ });
+
+ animator.start();
+ }
+}
new file mode 100644
index 0000000000000000000000000000000000000000..6cde348254185728ce5e7c11acb08a8d27d85831
GIT binary patch
literal 1645
zc$@)k29o)SP)<h;3K|Lk000e1NJLTq002k;002k`1ONa4|Kxkj000IwNkl<ZcmeI&
z#g-hu702;URrRok%@j^n;pfOMyD)45+rT!Qr^r65JVzK-e+J|HVX)nHLrDtP=~G>!
zKF*B8vOShcQs3|W-7Dzs@&EGw^50ep)1_2dTI!8@3eUczrFv8w31T<Uf~9ZY{^nOV
zK2Yt5NKJ17Q0p;+o0?t-G^N4<%En=U9`;Y}{OAWizGI}8+CZh%AAjgu-^9qec8M(|
zXZxC;>S?2T%BglUJN1gn$WjVF`R<p0;juV91S*@K{-nNvkpmZ|G)$sWY+#}aNg*ka
zK&175iL${dP&TS5D$pxdKk;eL8_fbORhyTtVr1Y@P{A}-%g|qdTD|RdfkLTO8r0H?
za`#rX8BR=EKuf*R`eNnCiJ@TMwuGY5pecf+ppDglG_BVEZLmTmS}Zrh`cgI00vh!!
z8X6iK3&vJeip+|HkX^Z{)s9W8!B-j=V~eAPqc_c~HZ&1L9NTWa<a8@_VYbF;z`8M$
zq&7BIDQ1CSY$6DfLf@*zjzBm^%t91I=mx3<wTOfRmr=}`IY-PW=754AAc7|bR=ZR7
zoG=SSbth26n}lZKfhA?9sq?`s5jh<w38K^t8auYxE%!NLCQ&#Yh%~Jb4z21z=MHmL
z%t8sd9E#F3vBF!fbnilE#cWoYyh1^mp6Ib->)i&=hS{hjx9o{RYuJHhy>5eN#7qh)
zmqUdhNMbVuk~dxLHhDJ8`5}u!dtYe6w$W|!OqkJvB+lE1(#)|Kyk)UDNHlWeOqhiz
zNcp6m%^~2S8_I4|XTVJ4lR7o0OWB$_vgew-(WrbX%u>vUEQxASLYNV|Z`o$X*TUGq
z)|4yr)lBn%f+UE_&X;*p&x+#Ikn}up=%RHLc{Q8fm<?I8=%cPudf=ActU66?%RPHe
z>{`sIGmRP4xqT>_bl{XztMEaS++BC)1#9{S26k+t$R>CxNXq<K7EPp<Pt!^M#C<n9
zQ#OGeeKp?Jcga#l%_J%2--n_o&3I01YL7g0<<wLyY&t;n#2puHovJ2<!2J7AoV$}8
z;qr{X02@w>0dMNNrtDI)30gG&aGmi5N|3;Tp3P2uVB7uX$s@10W~EcjQX+pri_KP*
z3mSzztfTBubJ>QYsd;zp+UZm?isXTUAa?uSsy%SkM#lyU+fD?DsnA_lwO%pJ1FF@c
zDCSX`+I`Wc?rgi{zUe0euexEmrB9K+Q#ZSH9%UA(<L%9g4Tq*jkG<~l)M!d1^7~Ne
zPU`L_T}VxB$H36^yy$@^F1Ib_Z;VlDg=`+weG@x$tj5dkh^^84M$JZ$L`gAk+1F$N
zW@~$%xYj%gO>My?4`)W77`vgTkW~4dI*Edq4_Rm5p^=-anT3791t&A}9(&nME6RBa
zrGF@7K;LVwS*8`dt41Erj1PYqz16s{8z@Ro8M5}mzG@4Y3f>K`8+OdQXW*U8yD>(D
z96$y$y6cgv3e(A5xb+vL+3`nS@gXs<P*6)*kbG@l-wmrRw{CmQu)~>0UbUG~r~z{Z
zq@kB>xS^VMtWl{wKHAGEG*Sy9XF}r0BfEB$&>-D2m>#EA84Ed}LhL*`o*rpz&%Vpn
z^_rer-u7nGA_++OWuGK*zR7w0O_Eo3?N|WpT6W(A6e?-nXN&|v${2rI%xKS^4cpei
zviEq~zJNj`zshP8aXt`Fol{48Hmq5*=%P(;I}%JxGCA~@?jjF#hNlzz_OW8cnk|<+
zGEf_z4m4DY=MOZqqYv!EU-Y_QDF-xGBj!I#JvYq9hWRV(iIHfM$2mvLXu;5l#cm)5
zh9+X(>^Wi<Y9oWwf%?XRl>N5id@+-e!ge<hBlS#5H(<{MGhvup_Cd;<KYz^J(J@x!
zzjS*hn4$X%?<9PFQJnfDHi@lvu6Mc>+pXNVpH+f1llMzoqYqU5`bD(+^6h-@{8sVt
zcl~B%>G{!7OQBJy`o;S{zbrL>8m~I_N#~>H*O&kB?y5fuv#bj)dw=oH-9PkS9`>`o
zH&4#0=GQO$^!EOT(raGurb)-ITDa`C+b&vFdG;lE>}_}4@utV(?B3b4WV6}~>lXh>
rK;x<(jvX7RrGL|3*ZF_>k1Kx!zN3VBQJn+S00000NkvXXu0mjfdU-nq
new file mode 100644
index 0000000000000000000000000000000000000000..29ff5b0f1d90a1e419f85579c80a50a7d48730ed
GIT binary patch
literal 3167
zc$@)W450IgP)<h;3K|Lk000e1NJLTq002k;002k`1ONa4|Kxkj000aoNkl<ZcmeFw
zh0^o58U*3zm1M~B{om&g`C@BMmCi!ZGQ-3SaaWJzzx<a!FKMJwkiO2=>Q|+H83-uq
z&RF{wq_n!zYNneCsrbE<4%tXcCwum^46>zVXRVb=_p{Ud%wz~@T6WR!a%)D0j+=S|
z%_QS@tSlW}>XlJfn%$+lZ7!oeklo7=&4sO=ey^l5$WB^{oH^I_k(QA?J!jggPLA~S
zY><)hyBV?!RC0QPL-$n5jt{y`d~_^Zr&ryn<uTO!4T09oDO<f|S7MqA=T?Txw42^+
zYVCKtnV=MP{f2`&3#XzobT7?eb8AY|up5R?7EWh9H4TR>7#V55k)brDG`DCN3uc&x
zr)_O_x9(EnmUVn<UJf7ix_*Z-v^))`r&AMU^U;ahX0j^dsSF=Q6VoM1YLZel{rt+(
zNK1V;N3z@UuzB+^m!HGsRJw=5uKPlX&n%-(Lu|@kw~;-Eg_Hg>45fjn<>NNcy=-e;
zf3I*^KrT($TuKRUmkHiu+C5!1<*HYPDM}Nm`NM`*EgIBJtMyUSQ`da13_};?^zfx&
z^M%bPKGT(^Je*F4xvqbzG!HyoDfEh>?waXoKL}KlrM0Jo`e+s8)Rm61&TWP`ef2-a
z%mGZYB#Xj-#6!Ajs<ZZ+wr#I%+v8c=wr$(CZQFLuwtHHsr-<D>Uwp&NosO)kbiVWM
zJ-CqtOntHQwJtR}wCLdRB!ZgimXp@1a1x0rd>vz?JX2x39pAE`CR6$dOcm0*fq?)e
zAtJ_7==_a%1Wj_HTRsRr|7x|T#Ly?E)UAw48q6ulusVs-<e_niV#r>$ahJ7q!SGgp
zBO)EnQvwYEu_RbAs75OORAN;8tEu`u;8!2sI+V5?XVBN{N@-F_2bY*a>7`6lkON=@
zX$&B*ns^@*gBkfI1qg^&eKHzK_9;QD;b`j>&G(<RqYdvrYwO!+F(jp+1@uB?hZL$&
zOu|rAs;IC&Ree&Nsm5~1crUt{QqUR{IwLJ*R7+qI3A{#4@@_Hx*Q+(|w2f^v?)29)
zWdVqBjs{I2rA3R9CJljdYIj0xa*R(+Zpw+W_g1aRi6|8Yg$`hYwrn_1Tb9?Rk_*{q
zpSAoRJAt3^xA#B&WAup_5Rp^pdZ4&<fmek-9a%zRa!@3VQ<=9uHqPWY)g_TSB7F!d
zaT#E>#Hn^L!zL|zljWSrnJg#1DGpUd@+Z_X0)dx5>DrQ0)2ta2dn@r@o<s5-jbJ9%
zlT}bvr3`E1=n#k3*W@)7(<Jye+;rjWnY4j3SvdQ~o8A9=7*!_$1xO@vr^<e-%B09l
z^%qm1Kp9gZ-S(h9r^@t7ic;TIoroqKQrgs+miX)YZ@ZisQc`AE&c3@~bhAlIHEhtt
z1_+v#L}|q{@_ldBG=F84d65fgEX9)wt`Vyuyc*^ao%L7^h~IF{CEfY7u|S*oEa|@C
z1`qf?C{S$(+$K)yizq;iG-8z^1W0^g{FN#c98<=n7dnAS)ig0j)shDX+1KEI^_fQ+
zjx&$t|9wRHUYCm-)fEY&p_e4Vi*%yF7M$^8NNpU}*Yr|^ULBUQk}z^b>P(_b3*hb7
zT;5&8l%7Umzli1Cw_jT@I%-Tqf#GDSQD9&qH**25#rR~L$|6&mEM&VSj13?%rr1|(
zdciecwf8<3Jm_u@I_F+bJ@3(HJ?}wp?4P%eS-pr*qgmF`Kkq?rJnMPqJ^Efxdf<5v
zxz~N}^WrVf*!7ByRB|Jv-nsw+f#`0G4_z`k;cK|dgr}!@$GaSP?gD0Lp%~ZSCNnHx
zAv4s`(i(A$eeCBL14fL<^taD3_A~qBkAH{}IYtJc%>e~yPOnTgb{>*7mVyHo>3|l=
zt8P_1bxC`gCJDMoKn?1$oJ*GG`w>HiWE9~t-~@BMc~3b1yA(oKF``Bv3k*X@yGei&
zrHfHz)OQ`ZM)ISb{bwyYbDDaPoz-Rha=KI^;)oMm^w;)__WpvN9vYFULa0`mfH0Or
z8Z521i21~zmMN<-d;hfcp}k*2O`((u)}&=6&jw8;aPJ3OAGh}}R8p*$isf)Z>p@#f
zGkJtfsA#GLDNgZV=#Ypuou54P%<IikJFVT5W5^MXd~w$s=;Js=&VT_ir-=24VJO6&
z5@V_>rM))DA~G(#+$qHxM5H45X!n~=ymKEzDz0i(^w>W*_0C;y!zpS@)yIZG#sed{
zU14%yj4{d@gI7RB4gwKVhOQ5ZVe@yYm;P&TO71ync?wSQ??L?1&0i1EjB-&!8%0Pm
zz+_Uk9h>&XVN7O5lBUvH9+ZjRgzd3swDxz6m+qVEO|Ft=js0_rpRoKQoru9fh+0D~
zXvj9buX&>p&5d(vM6ocOW0Y4Fu|N$Jw~hf8mjA2yj-wPzS_KzVe!7zZggtMjDqC5K
zSQGYaIg!bY^L9+jd?Zj6PQ5mIdzgg6{!>jOhCeJiE<ls{^S7m{Ly1$p!@fg8@r1ou
z*i={;Qz7(yp^VWfjWa>=C=3O#AZDmTxPPvn3`aHMz#1SEoI-+2wO<+Ou@T^LW`qH}
zk6;WSZ45!X4%z!iA}w$_f)D-o42VbTl(-XCM4fV-h+FBjay48@rbi;lz3?gCr^Zhh
z@<Y=KKvsywg=j6+^-7mh*k4Rdbvq<XS_y5sd~y{9Ms-1TvU<M@b+3%aY1G#m*D|Lp
z!%GG7AkEdvN5>#|2aJBWdPZBd2}?7}JPt0ARrIJ-9ZM#~#dtv~e2m5U*hdopq1`r-
zsYuYS9^F#NA`VsJ#i+M#I!uZ39Hsd&`syp7DD2Q*ucno<k&2O+sQLhzIF#c}X{!JK
zWrJF#<D>#%$t7CHfJ!D!yljf}5(C`F-1poQ+!N-$w~vzog~sk^J(-$?%r(M@0A=`V
z!VJ$<eA(b55V;-bV}?4vSY%Z5{#I+P=n!23ru~HV@9ZAw9$Wv`_9dVqTAb1P&J06P
zbxKM-T#<_iHB}4DctBU$-;01y1?E`#(p*W*;7c2)r2l=)eJg#)=GS)*t1S-GJ+S$W
z=|kqewNLV#=Ev(9D8)3XVME>fAqTvUb{b#lX*cO>AT=~Z!lpvEfgyeNW4GL6CFaZX
zKD6iv4rr-%MnyB^*tzNBXZxG}i=TbwY0t#b!{g|yRY9%#ntkjt6X4YG5GJE0X#bgL
zb7&v+!R@^b@80usAkpZfKo&IQTG}Z7VAG%O^PBdI?je;$s!=4>MH(i-O8BP(NT5=a
z`$V-#lx;cfFo;xd*?OP9hAGyDZE}rFoQUwstVBE3(OZ_^=P%^+Ks0v2OuY904srH0
z04ev;e=`!Gi4LS1Rm2(@#Q|bH{-1WuLx+(N2BIQjN%{XTy}s3#_cYgguSnang~1qm
z7=n$Euz*B`QxXQgQen-TupeU=B0X^0Mh%~7A;B=N)j$A(xDpU}6-StWW96liUsm0c
zW@aFUSDo%w>Y-$_x^@%Pdl=07!)?)JPKcId{Y&(F62THHY&dYlgp0)VNE$vuRFcyj
zbc?X{O5n7$+85~5ZHa{pXQ5af7!kvFf}Y?Ot9mGFnzhfyrad0X4xWaBbkhea>;W8H
z`tzL{XWr>&K@a2g&G}T3$}>p<bO{=2vL(7;2^|}b7$K$${6a320m&>r8&KSOior|g
zyl@sJI$EsOLVBY5CvZWAc2uAfjfjj46hKm(0MS&4>*rgj6D649CK8nuFk%E2?b(h@
z;vHd8dl-06&f>^7h-FVzh<2!8bo|fqAv&yDNiBEnF}EkC)q~IJNodhHHP=rKdb=Kj
zH6PvWGZTGsAI+rjIBH`nWpm(s<|xYoi2{>Cy5e~lbqiHILUqrPrN+y6T%qrC))~)k
z3twt;HHDp!j?c7^ZP?p_@oO^Oj%Yd9gG7oGFQM@QCvjrI#9&fsMrC^<yi@g9==l2o
zEp(36&`CYJaAaSjZdnArIk&o1bLqEvXVjJZb)_jR4@c(VYGjuDbp=R}OpfgNsGse^
zyyWi#L-^;qaGxdsoDGn?1q{!o>fikTj<kpm4%>Umg1MUjrjm<mp<W$;)pCcY^PN{`
z-w4X;tWtZW1KGHu8n)bjN!oKqTa~{F^B0%@<$w8Ko&&dWTq@l3@?ro0002ovPDHLk
FV1i0m8GZl&
new file mode 100644
index 0000000000000000000000000000000000000000..4cd8e3c07123f4599ef408db0389e2b63d9eba4f
GIT binary patch
literal 537
zc$@(l0_OdRP)<h;3K|Lk000e1NJLTq000;O001Tk1ONa4bPkiQ0005tNkl<ZcmeH_
z1MJ;U6o=1j+m88y*$d`iwxiik^=)6>Mhj=#Mx4wx|Fdn~<vx{DG=5)o&iSQ=|NJ~`
zmDB`%r}fOnHntvU>r*3`((pBy(u!_lqV>!;ZXCDXWhyxee)u{7Vs8suSU)PXBUJ{j
znKa9MO}b)l&zX%YY-6FF97inxQj@)Bq>9QaIhDFOe{(`N6YCdIFVt7)q8~Le8v!5s
zMH7<@RTK!~R1-VOaR`9MWM^}LAk8wcqjnyRLj#3w;6Xc`nKI=`abCxM<(=R`oSNyg
z5kP%|acN$nB6#4MWhO#9kXC5eD;g90oWgYV4Vg*vK-#HdN9CP-ooyWQNgu19(uq$X
zZdI_OU;so*KmhrAW-h-0@wki~(QXJux{;YnmjSJlv0Jo@ZqG)zgm^$aK()-o>Gtx+
z>~s^*!&5k1=`B4L(ul`%J$e|#Yf9MTTe21JQj;KqD}i36|0iyrh#k=@YErBLxL_B%
zW<|*vT9Cp~C}6Z`9TH7cq@(-^*W_I89BUEw%b(y53eUa0L~qD2vp9ECz~3`LfkU(o
zckx?c{!M%oc!`#v*YNmj&h7wV#7D8$gJI@GtQUNZnZ;w1k}Ij*>~-c0wLUpNL6LL+
be*EVP1h@O8Hf<mg00000NkvXXu0mjf07LZ9
new file mode 100644
index 0000000000000000000000000000000000000000..55462df1dc0e7aeecf6cff11acb9ce48364358e8
GIT binary patch
literal 151
zc%17D@N?(olHy`uVBq!ia0vp^96-#?!2%>-Ok!CFq~bhX978G?dpnLYIynkB7iM-^
zznl6nic9b7gR-XY<>ux#!m{%n8X7noEe;=lTerYHrvKigDLW0fe7O<kG2`^%NPgpz
z5ATzsOE)vH+rG-?diLzXb!De+;q+feYS({YVEi5|E5OnMvWdad)z4*}Q$iB}StvK;
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/res/values/dimens.xml
@@ -0,0 +1,222 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <dimen name="standard_corner_radius">4dp</dimen>
+
+ <dimen name="autocomplete_min_width">200dp</dimen>
+ <dimen name="autocomplete_row_height">32dp</dimen>
+
+ <dimen name="browser_toolbar_height">48dp</dimen>
+ <!-- This value is the height of the Tabs Panel header view
+ (browser_toolbar_height) minus the height of the indicator
+ (6dp). This value should change when the height of the view changes. -->
+ <dimen name="tabs_panel_indicator_selected_padding_top">42dp</dimen>
+
+ <!-- We use two different values for browser_toolbar_height on tablet
+ which is inconsistent. Temporary value until bug 1150730 is fixed. -->
+ <dimen name="browser_toolbar_height_flipper">48dp</dimen>
+ <dimen name="browser_toolbar_button_padding">12dp</dimen>
+ <dimen name="browser_toolbar_icon_width">48dp</dimen>
+ <dimen name="browser_toolbar_menu_icon_height">16dp</dimen>
+
+ <!-- favicon_size includes 4dp of right padding. We can't use margin (which would allow us to
+ specify the actual size) because that would decrease the size of our hit target. -->
+ <dimen name="browser_toolbar_favicon_size">21.33dip</dimen>
+ <dimen name="browser_toolbar_shadow_size">2dp</dimen>
+
+ <!-- If you update one of these values, update the others. -->
+ <dimen name="tablet_nav_button_width">42dp</dimen>
+ <dimen name="tablet_nav_button_width_half">21dp</dimen>
+ <dimen name="tablet_nav_button_width_plus_half">63dp</dimen>
+
+ <!-- This is the system default for the vertical padding for the divider of the TabWidget.
+ Used to mimic the divider padding on the tablet tabs panel back button. -->
+ <dimen name="tab_panel_divider_vertical_padding">12dp</dimen>
+
+ <dimen name="tablet_tab_strip_height">48dp</dimen>
+ <dimen name="tablet_tab_strip_item_width">208dp</dimen>
+ <dimen name="tablet_tab_strip_item_margin">-28dp</dimen>
+ <dimen name="tablet_tab_strip_fading_edge_size">15dp</dimen>
+ <dimen name="tablet_browser_toolbar_menu_item_width">56dp</dimen>
+ <!-- Padding combines with an 18dp image to form the menu item width and height. -->
+ <dimen name="tablet_browser_toolbar_menu_item_padding_horizontal">19dp</dimen>
+ <dimen name="tablet_browser_toolbar_menu_item_inset_vertical">5dp</dimen>
+ <dimen name="tablet_browser_toolbar_menu_item_inset_horizontal">3dp</dimen>
+ <dimen name="tablet_tab_strip_button_inset">5dp</dimen>
+
+ <!-- Dimensions used by Favicons and FaviconView -->
+ <dimen name="favicon_bg">32dp</dimen>
+ <dimen name="favicon_corner_radius">4dp</dimen>
+ <!-- Set the upper limit on the size of favicon that will be processed. Favicons larger than
+ this will be downscaled to this value. If you need to use larger Favicons (Due to a UI
+ redesign sometime after this is written) you should increase this value to the largest
+ commonly-used size of favicon and, performance permitting, fetch the remainder from the
+ database. The largest available size is always stored in the database, regardless of this
+ value.-->
+ <dimen name="favicon_largest_interesting_size">32dp</dimen>
+
+ <dimen name="firstrun_content_width">300dp</dimen>
+ <dimen name="firstrun_min_height">120dp</dimen>
+ <dimen name="firstrun_background_height">120dp</dimen>
+
+ <dimen name="overlay_prompt_content_width">260dp</dimen>
+ <dimen name="overlay_prompt_button_width">148dp</dimen>
+ <dimen name="overlay_prompt_container_width">@dimen/match_parent</dimen>
+
+ <!-- Site security icon -->
+ <dimen name="browser_toolbar_site_security_height">32dp</dimen>
+ <dimen name="browser_toolbar_site_security_width">32dp</dimen>
+ <dimen name="browser_toolbar_site_security_margin_right">0dp</dimen>
+ <dimen name="browser_toolbar_site_security_padding_vertical">7dp</dimen>
+ <dimen name="browser_toolbar_site_security_padding_horizontal">7dp</dimen>
+
+ <!-- If one of these values changes, they all should. -->
+ <dimen name="browser_toolbar_site_security_margin_bottom">.5dp</dimen>
+ <dimen name="site_security_unknown_inset_top">1dp</dimen>
+ <dimen name="site_security_unknown_inset_bottom">-1dp</dimen>
+
+ <dimen name="home_folder_title_oneline_textsize">16sp</dimen>
+ <dimen name="home_folder_title_twoline_textsize">14sp</dimen>
+ <dimen name="home_twolinepagerow_title_textsize">16sp</dimen>
+
+ <dimen name="page_row_edge_padding">16dp</dimen>
+
+ <!-- Regular page row on about:home -->
+ <dimen name="page_row_height">64dp</dimen>
+
+ <!-- Group/heading page row on about:home -->
+ <dimen name="page_group_height">56dp</dimen>
+ <dimen name="home_header_item_height">56dp</dimen>
+ <dimen name="page_row_divider_height">1dp</dimen>
+
+ <!-- Remote Tabs static view top padding. Less in landscape on phones. -->
+ <dimen name="home_remote_tabs_top_padding">48dp</dimen>
+
+ <!-- Remote Tabs Hidden devices row height -->
+ <dimen name="home_remote_tabs_hidden_footer_height">40dp</dimen>
+
+ <!-- Search Engine Row height -->
+ <dimen name="search_row_height">48dp</dimen>
+
+ <dimen name="doorhanger_width">300dp</dimen>
+ <dimen name="doorhanger_input_width">250dp</dimen>
+ <dimen name="doorhanger_offsetX">12dp</dimen>
+ <dimen name="doorhanger_offsetY">67dp</dimen>
+ <dimen name="doorhanger_drawable_padding">5dp</dimen>
+ <dimen name="doorhanger_subsection_padding">8dp</dimen>
+ <dimen name="doorhanger_section_padding_small">10dp</dimen>
+ <dimen name="doorhanger_section_padding_medium">20dp</dimen>
+ <dimen name="doorhanger_section_padding_large">30dp</dimen>
+ <dimen name="doorhanger_icon_size">60dp</dimen>
+ <dimen name="doorhanger_rounded_corner_radius">4dp</dimen>
+
+ <dimen name="context_menu_item_horizontal_padding">10dp</dimen>
+
+ <dimen name="flow_layout_spacing">6dp</dimen>
+ <dimen name="menu_item_icon">21dp</dimen>
+ <dimen name="menu_item_textsize">16sp</dimen>
+ <dimen name="menu_item_state_icon">18dp</dimen>
+ <!-- This is chosen to match Android's listPreferredItemHeight.
+ TODO: We should inherit these from the system.
+ http://androidxref.com/4.2.2_r1/xref/frameworks/base/core/res/res/values/themes.xml#123 -->
+ <dimen name="menu_item_row_height">64dip</dimen>
+ <dimen name="menu_item_row_width">240dp</dimen>
+ <dimen name="menu_popup_width">256dp</dimen>
+ <dimen name="nav_button_border_width">1dp</dimen>
+ <dimen name="prompt_service_group_padding_size">32dp</dimen>
+ <dimen name="prompt_service_icon_size">36dp</dimen>
+ <dimen name="prompt_service_icon_text_padding">10dp</dimen>
+ <dimen name="prompt_service_inputs_padding">16dp</dimen>
+ <dimen name="prompt_service_left_right_text_with_icon_padding">10dp</dimen>
+ <dimen name="prompt_service_top_bottom_text_with_icon_padding">8dp</dimen>
+ <dimen name="tabs_panel_indicator_width">60dp</dimen>
+ <dimen name="tabs_panel_button_width">48dp</dimen>
+ <dimen name="tabs_strip_height">48dp</dimen>
+ <dimen name="tabs_strip_button_width">100dp</dimen>
+ <dimen name="tabs_strip_button_padding">18dp</dimen>
+ <dimen name="tabs_strip_shadow_size">1dp</dimen>
+ <dimen name="text_selection_handle_width">47dp</dimen>
+ <dimen name="text_selection_handle_height">58dp</dimen>
+ <dimen name="text_selection_handle_shadow">11dp</dimen>
+ <dimen name="validation_message_height">50dp</dimen>
+ <dimen name="validation_message_margin_top">6dp</dimen>
+
+ <dimen name="tab_thumbnail_width">121dp</dimen>
+ <dimen name="tab_thumbnail_height">90dp</dimen>
+ <dimen name="tab_panel_column_width">129dp</dimen>
+ <dimen name="tab_panel_grid_padding">20dp</dimen>
+ <dimen name="tab_panel_grid_vspacing">20dp</dimen>
+ <dimen name="tab_panel_grid_padding_top">19dp</dimen>
+
+ <dimen name="tab_highlight_stroke_width">4dp</dimen>
+
+ <!-- PageActionButtons dimensions -->
+ <dimen name="page_action_button_width">32dp</dimen>
+
+ <!-- Banner -->
+ <dimen name="home_banner_height">72dp</dimen>
+ <dimen name="home_banner_close_width">42dp</dimen>
+ <dimen name="home_banner_icon_height">48dip</dimen>
+ <dimen name="home_banner_icon_width">48dip</dimen>
+
+ <!-- Icon Grid -->
+ <dimen name="icongrid_columnwidth">128dp</dimen>
+ <dimen name="icongrid_padding">16dp</dimen>
+
+ <!-- PanelRecyclerView dimensions -->
+ <dimen name="panel_grid_view_column_width">150dp</dimen>
+ <dimen name="panel_grid_view_horizontal_spacing">3dp</dimen>
+ <dimen name="panel_grid_view_vertical_spacing">3dp</dimen>
+ <dimen name="panel_grid_view_outer_spacing">3dp</dimen>
+
+ <!-- PanelItemView dimensions -->
+ <dimen name="panel_article_item_height">95dp</dimen>
+
+ <!-- Button toast dimenstions. -->
+ <dimen name="toast_button_corner_radius">2dp</dimen>
+
+ <!-- TabHistoryItemRow dimensions. -->
+ <dimen name="tab_history_timeline_width">3dp</dimen>
+ <dimen name="tab_history_timeline_height">14dp</dimen>
+ <dimen name="tab_history_favicon_bg">32dp</dimen>
+ <dimen name="tab_history_favicon_padding">5dp</dimen>
+ <dimen name="tab_history_favicon_border_enabled">3dp</dimen>
+ <dimen name="tab_history_favicon_border_disabled">1dp</dimen>
+ <dimen name="tab_history_combo_margin_left">15dp</dimen>
+ <dimen name="tab_history_combo_margin_right">15dp</dimen>
+ <dimen name="tab_history_title_fading_width">50dp</dimen>
+ <dimen name="tab_history_title_margin_right">15dp</dimen>
+ <dimen name="tab_history_title_text_size">14sp</dimen>
+ <dimen name="tab_history_bg_width">2dp</dimen>
+ <dimen name="tab_history_border_padding">2dp</dimen>
+
+ <!-- ZoomedView dimensions. -->
+ <dimen name="zoomed_view_toolbar_height">44dp</dimen>
+ <dimen name="drawable_dropshadow_size">3dp</dimen>
+
+ <!-- Find-In-Page dialog dimensions. -->
+ <dimen name="find_in_page_text_margin_left">5dip</dimen>
+ <dimen name="find_in_page_text_margin_right">12dip</dimen>
+ <dimen name="find_in_page_text_padding_left">10dip</dimen>
+ <dimen name="find_in_page_text_padding_right">10dip</dimen>
+ <dimen name="find_in_page_status_margin_right">10dip</dimen>
+ <dimen name="find_in_page_control_margin_top">2dip</dimen>
+ <dimen name="progress_bar_scroll_offset">1.5dp</dimen>
+
+ <!-- Matches the built-in divider height. fwiw, in the framework
+ I suspect this is a drawable rather than a dimen. -->
+ <dimen name="action_bar_divider_height">2dp</dimen>
+
+ <!-- http://blog.danlew.net/2015/01/06/handling-android-resources-with-non-standard-formats/ -->
+ <item name="match_parent" type="dimen">-1</item>
+ <item name="wrap_content" type="dimen">-2</item>
+
+ <item name="tab_strip_content_start" type="dimen">12dp</item>
+ <item name="firstrun_tab_strip_content_start" type="dimen">15dp</item>
+
+ <item name="notification_media_cover" type="dimen">128dp</item>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/res/values/ids.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <item type="id" name="tabQueueNotification"/>
+ <item type="id" name="tabQueueSettingsNotification" />
+ <item type="id" name="guestNotification"/>
+ <item type="id" name="original_height"/>
+ <item type="id" name="menu_items"/>
+ <item type="id" name="menu_margin"/>
+ <item type="id" name="recycler_view_click_support" />
+ <item type="id" name="range_list"/>
+ <item type="id" name="pref_header_general"/>
+ <item type="id" name="pref_header_privacy"/>
+ <item type="id" name="pref_header_search"/>
+ <item type="id" name="updateServicePermissionNotification" />
+ <item type="id" name="websiteContentNotification" />
+
+</resources>
new file mode 100644
--- /dev/null
+++ b/mv_geckoview.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+mkdir -p mobile/android/geckoview/src/main/java/org/mozilla/gecko
+for i in gfx mozglue permissions restrictions sqlite util ; do
+ hg mv mobile/android/base/java/org/mozilla/gecko/$i mobile/android/geckoview/src/main/java/org/mozilla/gecko
+done
+
+for i in \
+ AlarmReceiver.java \
+ AndroidGamepadManager.java \
+ ANRReporter.java \
+ BaseGeckoInterface.java \
+ ContextGetter.java \
+ CrashHandler.java \
+ EventDispatcher.java \
+ GeckoAccessibility.java \
+ GeckoAppShell.java \
+ GeckoBatteryManager.java \
+ GeckoEditable.java \
+ GeckoEditableClient.java \
+ GeckoEditableListener.java \
+ GeckoEvent.java \
+ GeckoHalDefines.java \
+ GeckoInputConnection.java \
+ GeckoJavaSampler.java \
+ GeckoNetworkManager.java \
+ GeckoProfile.java \
+ GeckoProfileDirectories.java \
+ GeckoScreenOrientation.java \
+ GeckoService.java \
+ GeckoSharedPrefs.java \
+ GeckoSmsManager.java \
+ GeckoThread.java \
+ GeckoView.java \
+ GeckoViewChrome.java \
+ GeckoViewContent.java \
+ InputMethods.java \
+ InputConnectionListener.java \
+ NSSBridge.java \
+ NotificationClient.java \
+ NotificationHandler.java \
+ NotificationService.java \
+ PrefsHelper.java \
+ ServiceNotificationClient.java \
+ SmsManager.java \
+ SurfaceBits.java \
+ TouchEventInterceptor.java \
+; do
+ hg mv mobile/android/base/java/org/mozilla/gecko/$i mobile/android/geckoview/src/main/java/org/mozilla/gecko/$i
+done
+
+mkdir -p mobile/android/geckoview/src/main/java/org/mozilla/gecko/widget
+hg mv mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java mobile/android/geckoview/src/main/java/org/mozilla/gecko/widget
+
+mkdir -p mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille
+hg cp mobile/android/thirdparty/com/googlecode/eyesfree/braille/selfbraille/*.java mobile/android/geckoview/src/main/java/com/googlecode/eyesfree/braille/selfbraille
+
+hg cp mobile/android/base/java/org/mozilla/gecko/annotation mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko
+hg cp mobile/android/base/java/org/mozilla/gecko/SysInfo.java mobile/android/geckoview/src/duplicate/java/org/mozilla/gecko
+
+mkdir -p mobile/android/geckoview/src/main/res/drawable-hdpi
+hg cp mobile/android/base/resources/drawable-hdpi/{home_bg,home_star,ic_status_logo}.png mobile/android/geckoview/src/main/res/drawable-hdpi
+
+mkdir -p mobile/android/geckoview/src/main/res/values
+hg cp mobile/android/base/resources/values/{dimens,ids}.xml mobile/android/geckoview/src/main/res/values
--- a/settings.gradle
+++ b/settings.gradle
@@ -24,20 +24,22 @@ if (json.substs.MOZ_BUILD_APP != 'mobile
// Set the Android SDK location. This is the *least specific* mechanism, which
// is unfortunate: we'd prefer to use the *most specific* mechanism. That is,
// local.properties (first 'sdk.dir', then 'android.dir') and then the
// environment variable ANDROID_HOME will override this. That's unfortunate,
// but it's hard to automatically arrange better.
System.setProperty('android.home', json.substs.ANDROID_SDK_ROOT)
include ':app'
+include ':geckoview'
include ':omnijar'
include ':thirdparty'
project(':app').projectDir = new File("${json.topsrcdir}/mobile/android/app")
+project(':geckoview').projectDir = new File("${json.topsrcdir}/mobile/android/geckoview")
project(':omnijar').projectDir = new File("${json.topsrcdir}/mobile/android/app/omnijar")
project(':thirdparty').projectDir = new File("${json.topsrcdir}/mobile/android/thirdparty")
if (json.substs.MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER) {
include ':bouncer'
project(':bouncer').projectDir = new File("${json.topsrcdir}/mobile/android/bouncer")
}