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

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

View file

@ -0,0 +1,176 @@
buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
apply plugin: 'com.android.library'
def VERSION_NAME = '0.0.1'
android {
compileSdkVersion 23
buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
defaultConfig {
// TODO: version GeckoView explicitly. We'd like to avoid
// mozconfig.substs.ANDROID_VERSION_CODE, which won't be intuitive to
// consumer (and advances very quickly on pre-release channels).
versionCode 1
versionName VERSION_NAME
targetSdkVersion 23
minSdkVersion 15
consumerProguardFiles 'proguard-rules.txt'
}
buildTypes {
withGeckoBinaries {
initWith release
}
withoutGeckoBinaries { // For clarity and consistency throughout the tree.
initWith release
}
}
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/thirdparty/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.
}
assets {
}
}
}
}
dependencies {
compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
}
task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
into("${project.buildDir}/generated/source/preprocessed_code")
from("${topobjdir}/mobile/android/base/generated/preprocessed") {
// AdjustConstants is included in the main app project.
exclude '**/AdjustConstants.java'
}
}
apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
android.libraryVariants.all { variant ->
variant.preBuild.dependsOn syncPreprocessedCode
// Like 'debug', 'release', or 'withGeckoBinaries'.
def buildType = variant.buildType.name
// It would be most natural for :geckoview to always include the Gecko
// binaries, but that's difficult; see the notes in
// mobile/android/gradle/with_gecko_binaries.gradle. Instead :app uses
// :geckoview:release and handles it's own Gecko binary inclusion.
if (buildType.equals('withGeckoBinaries')) {
configureVariantWithGeckoBinaries(variant)
}
// Javadoc and Sources JAR configuration cribbed from
// https://github.com/mapbox/mapbox-gl-native/blob/d169ea55c1cfa85cd8bf19f94c5f023569f71810/platform/android/MapboxGLAndroidSDK/build.gradle#L85
// informed by
// https://code.tutsplus.com/tutorials/creating-and-publishing-an-android-library--cms-24582,
// and amended from numerous Stackoverflow posts.
def name = variant.name
def javadoc = task "javadoc${name.capitalize()}"(type: Javadoc) {
description = "Generate Javadoc for build variant $name"
failOnError = false
destinationDir = new File(destinationDir, variant.baseName)
source = files(variant.javaCompile.source)
classpath = files(variant.javaCompile.classpath.files) + files(android.bootClasspath)
options.windowTitle("Mozilla GeckoView Android API $VERSION_NAME Reference")
options.docTitle("Mozilla GeckoView Android API $VERSION_NAME")
options.header("Mozilla GeckoView Android API $VERSION_NAME Reference")
options.bottom("© 2016 Mozilla. All rights reserved.")
options.links("http://docs.oracle.com/javase/7/docs/api/")
options.linksOffline("http://d.android.com/reference/", "$System.env.ANDROID_HOME/docs/reference")
// TODO: options.overview("src/main/java/overview.html")
options.group("Mozilla GeckoView", "org.mozilla.gecko*") // TODO: narrow this down.
exclude '**/R.java', '**/BuildConfig.java', 'com/googlecode/**'
}
task "javadocJar${name.capitalize()}"(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
task "sourcesJar${name.capitalize()}"(type: Jar) {
classifier 'sources'
description = "Generate Javadoc for build variant $name"
destinationDir = new File(destinationDir, variant.baseName)
from files(variant.javaCompile.source)
}
}
apply plugin: 'maven'
uploadArchives {
repositories.mavenDeployer {
pom.groupId = 'org.mozilla'
pom.artifactId = 'geckoview'
pom.version = VERSION_NAME
pom.project {
licenses {
license {
name 'The Mozilla Public License, v. 2.0'
url 'http://mozilla.org/MPL/2.0/'
distribution 'repo'
}
}
}
repository(url: "file://${project.buildDir}/maven")
}
}
// This is all related to the withGeckoBinaries approach; see
// mobile/android/gradle/with_gecko_binaries.gradle.
afterEvaluate {
// The bundle tasks are only present when the particular configuration is
// being built, so this task might not exist. (This is due to the way the
// Android Gradle plugin defines things during configuration.)
def bundleWithGeckoBinaries = tasks.findByName('bundleWithGeckoBinaries')
if (!bundleWithGeckoBinaries) {
return
}
// Remove default configuration, which is the release configuration, when
// we're actually building withGeckoBinaries. This makes `gradle install`
// install the withGeckoBinaries artifacts, not the release artifacts (which
// are withoutGeckoBinaries and not suitable for distribution.)
def Configuration archivesConfig = project.getConfigurations().getByName('archives')
archivesConfig.artifacts.removeAll { it.extension.equals('aar') }
artifacts {
// Instead of default (release) configuration, publish one with Gecko binaries.
archives bundleWithGeckoBinaries
// Javadoc and sources for developer ergononomics.
archives javadocJarWithGeckoBinaries
archives sourcesJarWithGeckoBinaries
}
}

View file

@ -0,0 +1,175 @@
# Modified from https://robotsandpencils.com/blog/use-proguard-android-library/.
# Preserve all annotations.
-keepattributes *Annotation*
# Preserve all public classes, and their public and protected fields and
# methods.
-keep public class * {
public protected *;
}
# Preserve all .class method names.
-keepclassmembernames class * {
java.lang.Class class$(java.lang.String);
java.lang.Class class$(java.lang.String, boolean);
}
# Preserve all native method names and the names of their classes.
-keepclasseswithmembernames class * {
native <methods>;
}
# Preserve the special static methods that are required in all enumeration
# classes.
-keepclassmembers class * extends java.lang.Enum {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Explicitly preserve all serialization members. The Serializable interface
# is only a marker interface, so it wouldn't save them.
# You can comment this out if your library doesn't use serialization.
# If your code contains serializable classes that have to be backward
# compatible, please refer to the manual.
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# Preserve all View implementations and their special context constructors.
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(...);
}
# Keep setters in Views so that animations can still work.
# See http://proguard.sourceforge.net/manual/examples.html#beans
# From tools/proguard/proguard-android.txt.
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# Preserve all classes that have special context constructors, and the
# constructors themselves.
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
# Preserve the special fields of all Parcelable implementations.
-keepclassmembers class * implements android.os.Parcelable {
static android.os.Parcelable$Creator CREATOR;
}
# Preserve static fields of inner classes of R classes that might be accessed
# through introspection.
-keepclassmembers class **.R$* {
public static <fields>;
}
# GeckoView specific rules.
# Keep classes, and all their contents, compiled before annotation.*.
-keep class org.mozilla.gecko.AppConstants {
*;
}
-keep class org.mozilla.gecko.AppConstants$Versions {
*;
}
-keep class org.mozilla.gecko.SysInfo {
*;
}
# Keep the annotation.
-keep @interface org.mozilla.gecko.annotation.JNITarget
# Keep classes tagged with the annotation.
-keep @org.mozilla.gecko.annotation.JNITarget class *
# Keep all members of an annotated class.
-keepclassmembers @org.mozilla.gecko.annotation.JNITarget class * {
*;
}
# Keep annotated members of any class.
-keepclassmembers class * {
@org.mozilla.gecko.annotation.JNITarget *;
}
# Keep classes which contain at least one annotated element. Split over two directives
# because, according to the developer of ProGuard, "the option -keepclasseswithmembers
# doesn't combine well with the '*' wildcard" (And, indeed, using it causes things to
# be deleted that we want to keep.)
-keepclasseswithmembers class * {
@org.mozilla.gecko.annotation.JNITarget <methods>;
}
-keepclasseswithmembers class * {
@org.mozilla.gecko.annotation.JNITarget <fields>;
}
# Keep WebRTC targets.
-keep @interface org.mozilla.gecko.annotation.WebRTCJNITarget
-keep @org.mozilla.gecko.annotation.WebRTCJNITarget class *
-keepclassmembers class * {
@org.mozilla.gecko.annotation.WebRTCJNITarget *;
}
-keepclassmembers @org.mozilla.gecko.annotation.WebRTCJNITarget class * {
*;
}
-keepclasseswithmembers class * {
@org.mozilla.gecko.annotation.WebRTCJNITarget <methods>;
}
-keepclasseswithmembers class * {
@org.mozilla.gecko.annotation.WebRTCJNITarget <fields>;
}
# Keep generator-targeted entry points.
-keep @interface org.mozilla.gecko.annotation.WrapForJNI
-keep @org.mozilla.gecko.annotation.WrapForJNI class *
-keepclassmembers class * {
@org.mozilla.gecko.annotation.WrapForJNI *;
}
-keepclasseswithmembers class * {
@org.mozilla.gecko.annotation.WrapForJNI <methods>;
}
-keepclasseswithmembers class * {
@org.mozilla.gecko.annotation.WrapForJNI <fields>;
}
# Keep all members of an annotated class.
-keepclassmembers @org.mozilla.gecko.annotation.WrapForJNI class * {
*;
}
# Keep Reflection targets.
-keep @interface org.mozilla.gecko.annotation.ReflectionTarget
-keep @org.mozilla.gecko.annotation.ReflectionTarget class *
-keepclassmembers class * {
@org.mozilla.gecko.annotation.ReflectionTarget *;
}
-keepclassmembers @org.mozilla.gecko.annotation.ReflectionTarget class * {
*;
}
-keepclasseswithmembers class * {
@org.mozilla.gecko.annotation.ReflectionTarget <methods>;
}
-keepclasseswithmembers class * {
@org.mozilla.gecko.annotation.ReflectionTarget <fields>;
}

View file

@ -0,0 +1,39 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.mozilla.geckoview">
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- READ_EXTERNAL_STORAGE was added in API 16, and is only enforced in API
19+. We declare it so that the bouncer APK and the main APK have the
same set of permissions. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-feature android:name="android.hardware.location" android:required="false"/>
<uses-feature android:name="android.hardware.location.gps" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen"/>
<!--#ifdef MOZ_WEBRTC-->
<!--<uses-permission android:name="android.permission.RECORD_AUDIO"/>-->
<!--<uses-feature android:name="android.hardware.audio.low_latency" android:required="false"/>-->
<!--<uses-feature android:name="android.hardware.camera.any" android:required="false"/>-->
<!--<uses-feature android:name="android.hardware.microphone" android:required="false"/>-->
<!--#endif-->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<!-- App requires OpenGL ES 2.0 -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
</manifest>

View file

@ -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(calledFrom = "ui", dispatchTo = "gecko")
private static native void notifyAlarmFired();
}

View file

@ -0,0 +1,425 @@
/* -*- 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.annotation.WrapForJNI;
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;
}
}
}
}
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
private static native void onGamepadChange(int id, boolean added);
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
private static native void onButtonChange(int id, int button, boolean pressed, float value);
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
private static native void onAxisChange(int id, boolean[] valid, float[] values);
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() {
}
@WrapForJNI
private static void start() {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
doStart();
}
});
}
/* package */ static void doStart() {
ThreadUtils.assertOnUiThread();
if (!sStarted) {
scanForGamepads();
addDeviceListener();
sStarted = true;
}
}
@WrapForJNI
private static void stop() {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
doStop();
}
});
}
/* package */ static void doStop() {
ThreadUtils.assertOnUiThread();
if (sStarted) {
removeDeviceListener();
sPendingGamepads.clear();
sGamepads.clear();
sStarted = false;
}
}
@WrapForJNI(calledFrom = "gecko")
private static void onGamepadAdded(final int device_id, final int service_id) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
handleGamepadAdded(device_id, service_id);
}
});
}
/* package */ static void handleGamepadAdded(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;
onButtonChange(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.
onAxisChange(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;
onButtonChange(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;
onButtonChange(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) {
sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
onGamepadChange(device.getId(), true);
}
private static void removeGamepad(int deviceId) {
Gamepad gamepad = sGamepads.get(deviceId);
onGamepadChange(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;
}
}

View file

@ -0,0 +1,169 @@
/* -*- 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;
private final EventDispatcher eventDispatcher;
public BaseGeckoInterface(Context context) {
mContext = context;
eventDispatcher = new EventDispatcher();
}
@Override
public EventDispatcher getAppEventDispatcher() {
return eventDispatcher;
}
@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 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) {}
// Bug 908781: Implement this
@Override
public void removePluginView(final View view) {}
@Override
public void enableOrientationListener() {}
@Override
public void disableOrientationListener() {}
// Bug 908786: Implement this
@Override
public void addAppStateListener(GeckoAppShell.AppStateListener listener) {}
// Bug 908787: Implement this
@Override
public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {}
// Bug 908789: Implement this
@Override
public void notifyWakeLockChanged(String topic, String state) {}
@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";
}
}

View file

@ -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();
}

View file

@ -0,0 +1,474 @@
/* -*- 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();
}
private static String getProcessName() {
try {
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) {
}
return null;
}
protected String getAppPackageName() {
final Context context = getAppContext();
if (context != null) {
return context.getPackageName();
}
// Package name is also the process name in most cases.
String processName = getProcessName();
if (processName != null) {
return processName;
}
// 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();
final String processName = getProcessName();
extras.putString("ProductName", pkgName);
extras.putLong("CrashTime", getCrashTime());
extras.putLong("StartupTime", getStartupTime());
extras.putString("AndroidProcessName", getProcessName());
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) {
return false;
}
};
}
}

View file

@ -0,0 +1,503 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy 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.ReflectionTarget;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.GeckoAppShell;
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";
/* package */ 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);
@ReflectionTarget
public static EventDispatcher getInstance() {
return INSTANCE;
}
public 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_OR_BETA && 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_OR_BETA) {
// 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_OR_BETA) {
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);
}
@ReflectionTarget
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);
}
private List<NativeEventListener> getNativeListeners(final String type) {
final List<NativeEventListener> listeners;
synchronized (mGeckoThreadNativeListeners) {
listeners = mGeckoThreadNativeListeners.get(type);
}
return listeners;
}
private List<GeckoEventListener> getGeckoListeners(final String type) {
final List<GeckoEventListener> listeners;
synchronized (mGeckoThreadJSONListeners) {
listeners = mGeckoThreadJSONListeners.get(type);
}
return listeners;
}
public boolean 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 true; // It may seem odd to return true here, but it's necessary to preserve the correct behavior.
}
final List<NativeEventListener> listeners = getNativeListeners(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. Return a failure rather than
// looking for JSON listeners. This is an optimization, as we can safely assume
// that an event which previously had native listeners will never have JSON
// listeners.
return false;
}
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 true;
}
// 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 (dispatchToThreads(type, message, /* bundle */ null, callback)) {
// 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 true;
}
try {
// If we didn't find native listeners, try JSON listeners.
return 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);
}
return true;
}
/**
* Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
*
* @param message Bundle message with "type" value specifying the event type.
*/
public void dispatch(final Bundle message) {
dispatch(message, /* callback */ null);
}
/**
* Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
*
* @param message Bundle message with "type" value specifying the event type.
* @param callback Optional object for callbacks from events.
*/
public void dispatch(final Bundle message, final EventCallback callback) {
if (message == null) {
throw new IllegalArgumentException("Null message");
}
final String type = message.getCharSequence("type").toString();
if (type == null) {
Log.e(LOGTAG, "Bundle message must have a type property");
return;
}
dispatchToThreads(type, /* js */ null, message, /* callback */ callback);
}
/**
* Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
*
* @param type Event type
* @param message Bundle message
*/
public void dispatch(final String type, final Bundle message) {
dispatch(type, message, /* callback */ null);
}
/**
* Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
*
* @param type Event type
* @param message Bundle message
* @param callback Optional object for callbacks from events.
*/
public void dispatch(final String type, final Bundle message, final EventCallback callback) {
dispatchToThreads(type, /* js */ null, message, /* callback */ callback);
}
private boolean dispatchToThreads(final String type,
final NativeJSObject jsMessage,
final Bundle bundleMessage,
final EventCallback callback) {
if (dispatchToThread(type, jsMessage, bundleMessage, callback,
mUiThreadListeners, ThreadUtils.getUiHandler())) {
return true;
}
if (dispatchToThread(type, jsMessage, bundleMessage, callback,
mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) {
return true;
}
if (jsMessage == null) {
Log.w(LOGTAG, "No listeners for " + type + " in dispatchToThreads");
}
if (!AppConstants.RELEASE_OR_BETA && jsMessage == null) {
// We're dispatching a Bundle message. Because Gecko thread listeners are not
// supported for Bundle messages, do a sanity check to make sure we don't have
// matching Gecko thread listeners.
boolean hasGeckoListener = false;
synchronized (mGeckoThreadNativeListeners) {
hasGeckoListener |= mGeckoThreadNativeListeners.containsKey(type);
}
synchronized (mGeckoThreadJSONListeners) {
hasGeckoListener |= mGeckoThreadJSONListeners.containsKey(type);
}
if (hasGeckoListener) {
throw new IllegalStateException(
"Dispatching Bundle message to Gecko listener " + type);
}
}
return false;
}
private boolean dispatchToThread(final String type,
final NativeJSObject jsMessage,
final Bundle bundleMessage,
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 + " in dispatchToThread");
// There were native listeners, and they're gone.
return false;
}
final Bundle messageAsBundle;
try {
messageAsBundle = jsMessage != null ? jsMessage.toBundle() : bundleMessage;
} 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 boolean dispatchEvent(final JSONObject message, final EventCallback callback) {
// {
// "type": "value",
// "event_specific": "value",
// ...
try {
final String type = message.getString("type");
final List<GeckoEventListener> listeners = getGeckoListeners(type);
if (listeners == null || listeners.isEmpty()) {
Log.w(LOGTAG, "No listeners for " + type + " in dispatchEvent");
return false;
}
for (final GeckoEventListener listener : listeners) {
listener.handleMessage(type, message);
}
} catch (final JSONException e) {
Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
}
return true;
}
@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);
}
}
/* package */ 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);
}
}
}
}

View file

@ -0,0 +1,410 @@
/* -*- 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.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.ViewParent;
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(context, 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"));
event.setToIndex(message.optInt("toIndex", -1));
event.setScrollable(message.optBoolean("scrollable"));
event.setScrollX(message.optInt("scrollX", -1));
event.setScrollY(message.optInt("scrollY", -1));
event.setMaxScrollX(message.optInt("maxScrollX", -1));
event.setMaxScrollY(message.optInt("maxScrollY", -1));
}
private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
final Context context = GeckoAppShell.getApplicationContext();
final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
accEvent.setClassName(GeckoAccessibility.class.getName());
accEvent.setPackageName(context.getPackageName());
populateEventFromJSON(accEvent, message);
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.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 View 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.getApplicationContext().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);
((ViewParent) 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(View view) {
// Only use this delegate in Jelly Bean.
if (Versions.feature16Plus) {
view.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
view.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(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.getApplicationContext().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;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,202 @@
/* -*- 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;
import org.mozilla.gecko.annotation.WrapForJNI;
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;
}
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
private static native void onBatteryChange(double level, boolean charging,
double remainingTime);
@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())) {
onBatteryChange(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() };
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
/* -*- 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);
// The following value is used by requestCursorUpdates
// ONE_SHOT calls updateCompositionRects() after getting current composing character rects.
public static final int ONE_SHOT = 1;
// START_MONITOR start the monitor for composing character rects. If is is updaed, call updateCompositionRects()
public static final int START_MONITOR = 2;
// ENDT_MONITOR stops the monitor for composing character rects.
public static final int END_MONITOR = 3;
void requestCursorUpdates(int requestMode);
}

View file

@ -0,0 +1,43 @@
/* -*- 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.graphics.RectF;
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();
void onTextChange();
void onDefaultKeyEvent(KeyEvent event);
void updateCompositionRects(final RectF[] aRects);
}

View file

@ -0,0 +1,27 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
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;
};

View file

@ -0,0 +1,491 @@
/* -*- 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.annotation.WrapForJNI;
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;
// We hackishly (yet harmlessly, in this case) keep a Context reference passed in via the start method.
// See context handling notes in handleManagerEvent, and Bug 1277333.
private Context context;
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 ConnectionType previousConnectionType = ConnectionType.NONE;
private ConnectionSubType currentConnectionSubtype = ConnectionSubType.UNKNOWN;
private ConnectionSubType previousConnectionSubtype = ConnectionSubType.UNKNOWN;
private NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
private NetworkStatus previousNetworkStatus = NetworkStatus.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(final Context context) {
this.context = context;
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;
}
// We're being deliberately careful about handling context here; it's possible that in some
// rare cases and possibly related to timing of when this is called (seems to be early in the startup phase),
// GeckoAppShell.getApplicationContext() will be null, and .start() wasn't called yet,
// so we don't have a local Context reference either. If both of these are true, we have to drop the event.
// NB: this is hacky (and these checks attempt to isolate the hackiness), and root cause
// seems to be how this class fits into the larger ecosystem and general flow of events.
// See Bug 1277333.
final Context contextForAction;
if (context != null) {
contextForAction = context;
} else {
contextForAction = GeckoAppShell.getApplicationContext();
}
if (contextForAction == null) {
Log.w(LOGTAG, "Context is not available while processing event " + event + " for state " + currentState);
return false;
}
performActionsForStateEvent(contextForAction, 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(final Context context, final ManagerState currentState, final 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(context);
registerBroadcastReceiver(context, this);
}
if (event == ManagerEvent.enableNotifications) {
updateNetworkStateAndConnectionType(context);
}
break;
case OnNoListeners:
if (event == ManagerEvent.receivedUpdate) {
updateNetworkStateAndConnectionType(context);
sendNetworkStateToListeners(context);
}
if (event == ManagerEvent.enableNotifications) {
updateNetworkStateAndConnectionType(context);
registerBroadcastReceiver(context, this);
}
if (event == ManagerEvent.stop) {
unregisterBroadcastReceiver(context, this);
}
break;
case OnWithListeners:
if (event == ManagerEvent.receivedUpdate) {
updateNetworkStateAndConnectionType(context);
sendNetworkStateToListeners(context);
}
if (event == ManagerEvent.stop) {
unregisterBroadcastReceiver(context, this);
}
/* no-op event: ManagerEvent.disableNotifications */
break;
case OffWithListeners:
if (event == ManagerEvent.start) {
registerBroadcastReceiver(context, this);
}
/* 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 context) {
final ConnectivityManager connectivityManager = (ConnectivityManager) context.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);
}
@WrapForJNI(dispatchTo = "gecko")
private static native void onConnectionChanged(int type, String subType,
boolean isWifi, int DHCPGateway);
@WrapForJNI(dispatchTo = "gecko")
private static native void onStatusChanged(String status);
/**
* Send current network state and connection type to whomever is listening.
*/
private void sendNetworkStateToListeners(final Context context) {
if (currentConnectionType != previousConnectionType ||
currentConnectionSubtype != previousConnectionSubtype) {
previousConnectionType = currentConnectionType;
previousConnectionSubtype = currentConnectionSubtype;
final boolean isWifi = currentConnectionType == ConnectionType.WIFI;
final int gateway = !isWifi ? 0 :
wifiDhcpGatewayAddress(context);
if (GeckoThread.isRunning()) {
onConnectionChanged(currentConnectionType.value,
currentConnectionSubtype.value, isWifi, gateway);
} else {
GeckoThread.queueNativeCall(GeckoNetworkManager.class, "onConnectionChanged",
currentConnectionType.value,
String.class, currentConnectionSubtype.value,
isWifi, gateway);
}
}
final String status;
if (currentNetworkStatus != previousNetworkStatus) {
previousNetworkStatus = currentNetworkStatus;
status = currentNetworkStatus.value;
} else {
status = LINK_DATA_CHANGED;
}
if (GeckoThread.isRunning()) {
onStatusChanged(status);
} else {
GeckoThread.queueNativeCall(GeckoNetworkManager.class, "onStatusChanged",
String.class, status);
}
}
/**
* Stop listening for network state updates.
*/
private static void unregisterBroadcastReceiver(final Context context, final BroadcastReceiver receiver) {
context.unregisterReceiver(receiver);
}
/**
* Start listening for network state updates.
*/
private static void registerBroadcastReceiver(final Context context, final BroadcastReceiver receiver) {
final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
context.registerReceiver(receiver, filter);
}
private static int wifiDhcpGatewayAddress(final 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; not part of the state machine flow.
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.
*
* Not part of the state machine flow.
*/
@JNITarget
public static int getMCC() {
return getNetworkOperator(InfoType.MCC, GeckoAppShell.getApplicationContext());
}
@JNITarget
public static int getMNC() {
return getNetworkOperator(InfoType.MNC, GeckoAppShell.getApplicationContext());
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,230 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy 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));
if (parser.getSections() != null) {
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);
if (parser.getSections() != null) {
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);
if (parser.getSections() != null) {
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
final INISection section = e.nextElement();
final String name = section.getStringProperty("Name");
if (name != null && name.equals(profileName)) {
if (section.getIntProperty("IsRelative") == 1) {
return new File(mozillaDir, section.getStringProperty("Path"));
}
return new File(section.getStringProperty("Path"));
}
}
}
throw new NoSuchProfileException("No profile " + profileName);
}
}

View file

@ -0,0 +1,423 @@
/* -*- 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.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()));
}
@WrapForJNI(dispatchTo = "gecko")
private static native void onOrientationChange(short screenOrientation, short angle);
/*
* 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;
}
if (GeckoThread.isRunning()) {
onOrientationChange(aScreenOrientation.value, getAngle());
} else {
GeckoThread.queueNativeCall(GeckoScreenOrientation.class, "onOrientationChange",
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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,677 @@
/* -*- 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;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
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 final Runnable UI_THREAD_CALLBACK = new Runnable() {
@Override
public void run() {
ThreadUtils.assertOnUiThread();
long nextDelay = runUiThreadCallback();
if (nextDelay >= 0) {
ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
}
}
};
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.shouldUseGuestMode(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);
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 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);
}
// 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() {
Log.i(LOGTAG, "preparing to run Gecko");
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() {
registerUiThread();
}
});
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");
GeckoAppShell.getGeckoInterface().getAppEventDispatcher().dispatchEvent(msg, null);
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);
}
@WrapForJNI(calledFrom = "gecko")
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(calledFrom = "gecko")
private static void setState(final State newState) {
ThreadUtils.assertOnGeckoThread();
synchronized (QUEUED_CALLS) {
flushQueuedNativeCallsLocked(newState);
sState = newState;
}
}
@WrapForJNI(calledFrom = "gecko")
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", dispatchTo = "gecko")
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", dispatchTo = "gecko")
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", dispatchTo = "gecko")
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);
}
}
// Implemented in mozglue/android/APKOpen.cpp.
/* package */ static native void registerUiThread();
@WrapForJNI(calledFrom = "ui")
/* package */ static native long runUiThreadCallback();
@WrapForJNI
private static void requestUiThreadCallback(long delay) {
ThreadUtils.getUiHandler().postDelayed(UI_THREAD_CALLBACK, delay);
}
}

View file

@ -0,0 +1,736 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import 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.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;
protected boolean onAttachedToWindowCalled;
protected String chromeURI = getGeckoInterface().getDefaultChromeURI();
protected int screenId = 0; // default to the primary screen
@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(dispatchTo = "proxy")
protected static final class Window extends JNIObject {
@WrapForJNI(skip = true)
/* package */ Window() {}
static native void open(Window instance, GeckoView view, Object compositor,
String chromeURI, int screenId);
@Override protected native void disposeNative();
native void close();
native void reattach(GeckoView view, Object compositor);
native void loadUri(String uri, int flags);
}
// 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];
}
};
}
protected 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;
if (stateBinder.window != null) {
this.window = stateBinder.window;
}
stateSaved = false;
if (onAttachedToWindowCalled) {
reattachWindow();
}
// 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);
}
protected void openWindow() {
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
Window.open(window, this, getCompositor(),
chromeURI, screenId);
} else {
GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, Window.class,
"open", window, GeckoView.class, this, Object.class, getCompositor(),
String.class, chromeURI, screenId);
}
}
protected void reattachWindow() {
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
window.reattach(this, getCompositor());
} else {
GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
window, "reattach", GeckoView.class, this, Object.class, getCompositor());
}
}
@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();
openWindow();
} else {
reattachWindow();
}
super.onAttachedToWindow();
onAttachedToWindowCalled = true;
}
@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");
}
onAttachedToWindowCalled = false;
}
@WrapForJNI public static final int LOAD_DEFAULT = 0;
@WrapForJNI public static final int LOAD_NEW_TAB = 1;
@WrapForJNI public static final int LOAD_SWITCH_TAB = 2;
public void loadUri(String uri, int flags) {
if (window == null) {
throw new IllegalStateException("Not attached to window");
}
if (GeckoThread.isRunning()) {
window.loadUri(uri, flags);
} else {
GeckoThread.queueNativeCall(window, "loadUri", String.class, uri, flags);
}
}
/* 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 handleReady(final JSONObject message) {
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);
}
}

View file

@ -0,0 +1,81 @@
/* -*- 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.os.Bundle;
public class GeckoViewChrome implements GeckoView.ChromeDelegate {
/**
* Tell the host application that Gecko is ready to handle requests.
* @param view The GeckoView that initiated the callback.
*/
@Override
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.
*/
@Override
public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
result.cancel();
}
/**
* 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.
*/
@Override
public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
result.cancel();
}
/**
* 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.
*/
@Override
public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) {
result.cancel();
}
/**
* 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.
*/
@Override
public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) {
result.cancel();
}
/**
* 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 cancel requests with a failed response.
*/
public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result) {
if (result != null) {
result.failure(null);
}
}
}

View file

@ -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;
public class GeckoViewContent implements GeckoView.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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {}
}

View file

@ -0,0 +1,52 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class GeckoViewFragment extends android.support.v4.app.Fragment {
private static final String LOGTAG = "GeckoViewFragment";
private static Parcelable state = null;
private static GeckoViewFragment lastUsed = null;
private GeckoView geckoView = null;
@Override
public void onCreate(Bundle savedInstanceState) {
setRetainInstance(true);
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
geckoView = new GeckoView(getContext());
return geckoView;
}
@Override
public void onResume() {
if (state != null && lastUsed != this) {
// "Restore" the window from the previously used GeckoView to this GeckoView and attach it
geckoView.onRestoreInstanceState(state);
state = null;
}
super.onResume();
}
@Override
public void onPause() {
state = geckoView.onSaveInstanceState();
lastUsed = this;
super.onPause();
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,17 @@
/* -*- 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 interface NotificationListener
{
void showNotification(String name, String cookie, String title, String text,
String host, String imageUrl);
void showPersistentNotification(String name, String cookie, String title, String text,
String host, String imageUrl, String data);
void closeNotification(String name);
}

View file

@ -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", dispatchTo = "gecko")
private static native void nativeGetPrefs(String[] prefNames, PrefHandler handler);
@WrapForJNI(stubName = "SetPref", dispatchTo = "gecko")
private static native void nativeSetPref(String prefName, boolean flush, int type,
boolean boolVal, int intVal, String strVal);
@WrapForJNI(stubName = "AddObserver", dispatchTo = "gecko")
private static native void nativeAddObserver(String[] prefNames, PrefHandler handler,
String[] prefsToObserve);
@WrapForJNI(stubName = "RemoveObserver", dispatchTo = "gecko")
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(calledFrom = "gecko")
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(calledFrom = "gecko")
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() {
}
}
}

View file

@ -0,0 +1,237 @@
/* -*- 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.os.StrictMode;
import android.util.Log;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.regex.Pattern;
/**
* A collection of system info values, broadly mirroring a subset of
* nsSystemInfo. See also the constants in AppConstants, which reflect
* much of nsIXULAppInfo.
*/
// Normally, we'd annotate with @RobocopTarget. Since SysInfo is compiled
// before RobocopTarget, we instead add o.m.g.SysInfo directly to the Proguard
// configuration.
public final class SysInfo {
private static final String LOG_TAG = "GeckoSysInfo";
// Number of bytes of /proc/meminfo to read in one go.
private static final int MEMINFO_BUFFER_SIZE_BYTES = 256;
// We don't mind an instant of possible duplicate work, we only wish to
// avoid inconsistency, so we don't bother with synchronization for
// these.
private static volatile int cpuCount = -1;
private static volatile int totalRAM = -1;
/**
* Get the number of cores on the device.
*
* We can't use a nice tidy API call, because they're all wrong:
*
* <http://stackoverflow.com/questions/7962155/how-can-you-detect-a-dual-core-
* cpu-on-an-android-device-from-code>
*
* This method is based on that code.
*
* @return the number of CPU cores, or 1 if the number could not be
* determined.
*/
public static int getCPUCount() {
if (cpuCount > 0) {
return cpuCount;
}
// Avoid a strict mode warning.
StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
try {
return readCPUCount();
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
private static int readCPUCount() {
class CpuFilter implements FileFilter {
@Override
public boolean accept(File pathname) {
return Pattern.matches("cpu[0-9]+", pathname.getName());
}
}
try {
final File dir = new File("/sys/devices/system/cpu/");
return cpuCount = dir.listFiles(new CpuFilter()).length;
} catch (Exception e) {
Log.w(LOG_TAG, "Assuming 1 CPU; got exception.", e);
return cpuCount = 1;
}
}
/**
* Helper functions used to extract key/value data from /proc/meminfo
* Pulled from:
* http://androidxref.com/4.2_r1/xref/frameworks/base/core/java/com/android/internal/util/MemInfoReader.java
*/
private static boolean matchMemText(byte[] buffer, int index, int bufferLength, byte[] text) {
final int N = text.length;
if ((index + N) >= bufferLength) {
return false;
}
for (int i = 0; i < N; i++) {
if (buffer[index + i] != text[i]) {
return false;
}
}
return true;
}
/**
* Parses a line like:
*
* MemTotal: 1605324 kB
*
* into 1605324.
*
* @return the first uninterrupted sequence of digits following the
* specified index, parsed as an integer value in KB.
*/
private static int extractMemValue(byte[] buffer, int offset, int length) {
if (offset >= length) {
return 0;
}
while (offset < length && buffer[offset] != '\n') {
if (buffer[offset] >= '0' && buffer[offset] <= '9') {
int start = offset++;
while (offset < length &&
buffer[offset] >= '0' &&
buffer[offset] <= '9') {
++offset;
}
return Integer.parseInt(new String(buffer, start, offset - start), 10);
}
++offset;
}
return 0;
}
/**
* Fetch the total memory of the device in MB by parsing /proc/meminfo.
*
* Of course, Android doesn't have a neat and tidy way to find total
* RAM, so we do it by parsing /proc/meminfo.
*
* @return 0 if a problem occurred, or memory size in MB.
*/
public static int getMemSize() {
if (totalRAM >= 0) {
return totalRAM;
}
// This is the string "MemTotal" that we're searching for in the buffer.
final byte[] MEMTOTAL = {'M', 'e', 'm', 'T', 'o', 't', 'a', 'l'};
// `/proc/meminfo` is not a real file and thus safe to read on the main thread.
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
try {
final byte[] buffer = new byte[MEMINFO_BUFFER_SIZE_BYTES];
final FileInputStream is = new FileInputStream("/proc/meminfo");
try {
final int length = is.read(buffer);
for (int i = 0; i < length; i++) {
if (matchMemText(buffer, i, length, MEMTOTAL)) {
i += 8;
totalRAM = extractMemValue(buffer, i, length) / 1024;
Log.d(LOG_TAG, "System memory: " + totalRAM + "MB.");
return totalRAM;
}
}
} finally {
is.close();
}
Log.w(LOG_TAG, "Did not find MemTotal line in /proc/meminfo.");
return totalRAM = 0;
} catch (FileNotFoundException f) {
return totalRAM = 0;
} catch (IOException e) {
return totalRAM = 0;
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
/**
* @return the SDK version supported by this device, such as '16'.
*/
public static int getVersion() {
return android.os.Build.VERSION.SDK_INT;
}
/**
* @return the release version string, such as "4.1.2".
*/
public static String getReleaseVersion() {
return android.os.Build.VERSION.RELEASE;
}
/**
* @return the kernel version string, such as "3.4.10-geb45596".
*/
public static String getKernelVersion() {
return System.getProperty("os.version", "");
}
/**
* @return the device manufacturer, such as "HTC".
*/
public static String getManufacturer() {
return android.os.Build.MANUFACTURER;
}
/**
* @return the device name, such as "HTC One".
*/
public static String getDevice() {
// No, not android.os.Build.DEVICE.
return android.os.Build.MODEL;
}
/**
* @return the Android "hardware" identifier, such as "m7".
*/
public static String getHardware() {
return android.os.Build.HARDWARE;
}
/**
* @return the system OS name. Hardcoded to "Android".
*/
public static String getName() {
// We deliberately differ from PR_SI_SYSNAME, which is "Linux".
return "Android";
}
/**
* @return the Android architecture string, including ABI.
*/
public static String getArchABI() {
// Android likes to include the ABI, too ("armeabiv7"), so we
// differ to add value.
return android.os.Build.CPU_ABI;
}
}

View file

@ -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);
}

View file

@ -0,0 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface JNITarget {}

View file

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*
* Used to indicate to ProGuard that this definition is accessed
* via reflection and should not be stripped from the source.
*/
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface ReflectionTarget {}

View file

@ -0,0 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface RobocopTarget {}

View file

@ -0,0 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface WebRTCJNITarget {}

View file

@ -0,0 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation is used to tag methods that are to have wrapper methods generated.
* Such methods will be protected from destruction by ProGuard, and allow us to avoid
* writing by hand large amounts of boring boilerplate.
*/
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface WrapForJNI {
/**
* Skip this member when generating wrappers for a whole class.
*/
boolean skip() default false;
/**
* Optional parameter specifying the name of the generated method stub. If omitted,
* the capitalized name of the Java method will be used.
*/
String stubName() default "";
/**
* Action to take if member access returns an exception.
* One of "abort", "ignore", or "nsresult". "nsresult" is not supported for native
* methods.
*/
String exceptionMode() default "abort";
/**
* The thread that the method will be called from.
* One of "any", "gecko", or "ui". Not supported for fields.
*/
String calledFrom() default "any";
/**
* The thread that the method call will be dispatched to.
* One of "current", "gecko", or "proxy". Not supported for non-native methods,
* fields, and constructors. Only void-return methods are supported for anything other
* than current thread.
*/
String dispatchTo() default "current";
}

View file

@ -0,0 +1,290 @@
/* -*- 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.net.MalformedURLException;
import java.net.URL;
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.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
public final class BitmapUtils {
private static final String LOGTAG = "GeckoBitmapUtils";
private BitmapUtils() {}
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(final Context context, final Uri resourceUrl) {
final String scheme = resourceUrl.getScheme();
if (!"drawable".equals(scheme)) {
// Return a "not found" default icon that's easy to spot.
return android.R.drawable.sym_def_app_icon;
}
String resource = resourceUrl.getSchemeSpecificPart();
if (resource.startsWith("//")) {
resource = resource.substring(2);
}
final Resources res = context.getResources();
int id = res.getIdentifier(resource, "drawable", context.getPackageName());
if (id != 0) {
return id;
}
// For backwards compatibility, we also search in system resources.
id = res.getIdentifier(resource, "drawable", "android");
if (id != 0) {
return id;
}
Log.w(LOGTAG, "Cannot find drawable/" + resource);
// Return a "not found" default icon that's easy to spot.
return android.R.drawable.sym_def_app_icon;
}
}

View file

@ -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 org.mozilla.gecko.mozglue.DirectBufferAllocator;
import android.graphics.Bitmap;
import android.util.Log;
import java.nio.ByteBuffer;
/** A buffered image that simply saves a buffer of pixel data. */
public class BufferedImage {
private ByteBuffer mBuffer;
private Bitmap mBitmap;
private IntSize mSize;
private int mFormat;
private static final String LOGTAG = "GeckoBufferedImage";
/** Creates an empty buffered image */
public BufferedImage() {
mSize = new IntSize(0, 0);
}
/** Creates a buffered image from an Android bitmap. */
public BufferedImage(Bitmap bitmap) {
mFormat = bitmapConfigToFormat(bitmap.getConfig());
mSize = new IntSize(bitmap.getWidth(), bitmap.getHeight());
mBitmap = bitmap;
}
private synchronized void freeBuffer() {
if (mBuffer != null) {
mBuffer = DirectBufferAllocator.free(mBuffer);
}
}
public void destroy() {
try {
freeBuffer();
} catch (Exception ex) {
Log.e(LOGTAG, "error clearing buffer: ", ex);
}
}
public ByteBuffer getBuffer() {
if (mBuffer == null) {
int bpp = bitsPerPixelForFormat(mFormat);
mBuffer = DirectBufferAllocator.allocate(mSize.getArea() * bpp);
mBitmap.copyPixelsToBuffer(mBuffer.asIntBuffer());
mBitmap = null;
}
return mBuffer;
}
public IntSize getSize() { return mSize; }
public int getFormat() { return mFormat; }
public static final int FORMAT_INVALID = -1;
public static final int FORMAT_ARGB32 = 0;
public static final int FORMAT_RGB24 = 1;
public static final int FORMAT_A8 = 2;
public static final int FORMAT_A1 = 3;
public static final int FORMAT_RGB16_565 = 4;
private static int bitsPerPixelForFormat(int format) {
switch (format) {
case FORMAT_A1: return 1;
case FORMAT_A8: return 8;
case FORMAT_RGB16_565: return 16;
case FORMAT_RGB24: return 24;
case FORMAT_ARGB32: return 32;
default:
throw new RuntimeException("Unknown Cairo format");
}
}
private static int bitmapConfigToFormat(Bitmap.Config config) {
if (config == null)
return FORMAT_ARGB32; /* Droid Pro fix. */
switch (config) {
case ALPHA_8: return FORMAT_A8;
case ARGB_4444: throw new RuntimeException("ARGB_444 unsupported");
case ARGB_8888: return FORMAT_ARGB32;
case RGB_565: return FORMAT_RGB16_565;
default: throw new RuntimeException("Unknown Skia bitmap config");
}
}
}

View file

@ -0,0 +1,35 @@
/* -*- 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 javax.microedition.khronos.opengles.GL10;
/** Information needed to render buffered bitmaps using OpenGL ES. */
public class BufferedImageGLInfo {
public final int internalFormat;
public final int format;
public final int type;
public BufferedImageGLInfo(int bufferedImageFormat) {
switch (bufferedImageFormat) {
case BufferedImage.FORMAT_ARGB32:
internalFormat = format = GL10.GL_RGBA; type = GL10.GL_UNSIGNED_BYTE;
break;
case BufferedImage.FORMAT_RGB24:
internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_BYTE;
break;
case BufferedImage.FORMAT_RGB16_565:
internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_SHORT_5_6_5;
break;
case BufferedImage.FORMAT_A8:
case BufferedImage.FORMAT_A1:
throw new RuntimeException("BufferedImage FORMAT_A1 and FORMAT_A8 unsupported");
default:
throw new RuntimeException("Unknown BufferedImage format");
}
}
}

View file

@ -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.MotionEvent;
import android.view.animation.LinearInterpolator;
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 = 150000000;
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 LinearInterpolator 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 LinearInterpolator();
// 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);
}
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()) {
if (layerViewTranslationNeeded == 0 && isResizing()) {
// We're already in the middle of a snap, so this new call is
// redundant as it's snapping to the same place. Ignore it.
return;
}
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;
}
}
}

View file

@ -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));
}
}

View file

@ -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
}

View file

@ -0,0 +1,694 @@
/* -*- 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.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.Color;
import android.graphics.Matrix;
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;
/*
* 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;
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;
/* package */ final PanZoomController mPanZoomController;
private final DynamicToolbarAnimator mToolbarAnimator;
/* package */ 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;
@WrapForJNI(stubName = "ClearColor")
private volatile int mClearColor = Color.WHITE;
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);
mCurrentViewTransform = new ViewTransform(0, 0, 1);
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);
}
public void setGeckoReady(boolean ready) {
mGeckoIsReady = ready;
}
@Override // PanZoomTarget
public boolean isGeckoReady() {
return mGeckoIsReady;
}
/** Attaches to root layer so that Gecko appears. */
@WrapForJNI(calledFrom = "gecko")
private void onGeckoReady() {
mGeckoIsReady = true;
mLayerRenderer = mView.getRenderer();
sendResizeEventIfNecessary(true, null);
// 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() {
mPanZoomController.attach();
mView.updateCompositor();
}
});
}
public void destroy() {
mPanZoomController.destroy();
mToolbarAnimator.destroy();
mDrawListeners.clear();
mGeckoIsReady = false;
}
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.notifySizeChanged(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);
}
/**
* 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
}
@WrapForJNI(calledFrom = "gecko")
void contentDocumentChanged() {
mContentDocumentIsDisplayed = false;
}
@WrapForJNI(calledFrom = "gecko")
boolean isContentDocumentDisplayed() {
return mContentDocumentIsDisplayed;
}
/** 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.
*/
@WrapForJNI
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 (i.e. mViewportMetrics).
// Usually this information is updated via handleViewportMessage
// while we remain on the same document.
setViewportMetrics(newMetrics, true);
// 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);
}
}
mContentDocumentIsDisplayed = true;
}
/** 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.
*/
private 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 (layersUpdated) {
for (DrawListener listener : mDrawListeners) {
listener.drawFinished();
}
}
return mCurrentViewTransform;
}
@WrapForJNI
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() {
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(calledFrom = "gecko")
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(calledFrom = "gecko")
public void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
synthesizeNativePointer(InputDevice.SOURCE_MOUSE, PointerInfo.RESERVED_MOUSE_POINTER_ID,
eventType, screenX, screenY, 0, 0);
}
@WrapForJNI
public LayerRenderer.Frame createFrame() {
// Create the shaders and textures if necessary.
if (!mLayerRendererInitialized) {
if (mLayerRenderer == null) {
return null;
}
mLayerRenderer.createDefaultProgram();
mLayerRendererInitialized = true;
}
try {
return mLayerRenderer.createFrame(mFrameMetrics);
} catch (Exception e) {
Log.w(LOGTAG, e);
return null;
}
}
private void geometryChanged() {
/* Let Gecko know if the screensize has changed */
sendResizeEventIfNecessary(false, null);
}
/** Implementation of LayerView.Listener */
@Override
public void surfaceChanged(int width, int height) {
IntSize viewportSize = mToolbarAnimator.getViewportSize();
setViewportSize(viewportSize.width, viewportSize.height, null);
}
ImmutableViewportMetrics getViewportMetrics() {
return mViewportMetrics;
}
/*
* 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();
}
}
/*
* 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 */
@Override
public void panZoomStopped() {
mToolbarAnimator.onPanZoomStopped();
}
Object getLock() {
return this;
}
Matrix getMatrixForLayerRectToViewRect() {
if (!mGeckoIsReady) {
return null;
}
ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
PointF origin = viewportMetrics.getOrigin();
float zoom = viewportMetrics.zoomFactor;
ImmutableViewportMetrics geckoViewport = mViewportMetrics;
PointF geckoOrigin = geckoViewport.getOrigin();
float geckoZoom = geckoViewport.zoomFactor;
Matrix matrix = new Matrix();
matrix.postTranslate(geckoOrigin.x / geckoZoom, geckoOrigin.y / geckoZoom);
matrix.postScale(zoom, zoom);
matrix.postTranslate(-origin.x, -origin.y);
return matrix;
}
@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);
}
public void setClearColor(int color) {
mClearColor = color;
}
}

View file

@ -0,0 +1,282 @@
/* -*- 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 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;
}
/** This constructor is used by native code in AndroidJavaWrappers.cpp, be
* careful when modifying the signature.
*/
@WrapForJNI(calledFrom = "gecko")
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)
{
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;
}
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));
}
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);
}
public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
return new ImmutableViewportMetrics(
pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
newOriginX, newOriginY, viewportRectWidth, viewportRectHeight,
zoomFactor);
}
public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
return new ImmutableViewportMetrics(
pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
newZoomFactor);
}
public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
}
public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
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);
}
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);
}
/* 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);
}
/** 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);
}
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;
}
}

View file

@ -0,0 +1,89 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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;
}
}

View file

@ -0,0 +1,275 @@
/* -*- 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.mozglue.DirectBufferAllocator;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.opengl.GLES20;
import android.os.SystemClock;
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";
/*
* The amount of time a frame is allowed to take to render before we declare it a dropped
* frame.
*/
private static final int MAX_FRAME_TIME = 16; /* 1000 ms / 60 FPS */
private static final long NANOS_PER_MS = 1000000;
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 long mLastFrameTime;
private final CopyOnWriteArrayList<RenderTask> mTasks;
// Dropped frames display
private final int[] mFrameTimings;
private int mCurrentFrame, mFrameTimingsSum, mDroppedFrames;
private IntBuffer mPixelBuffer;
private List<LayerView.ZoomedViewListener> mZoomedViewListeners;
private float mLastViewLeft;
private float mLastViewTop;
public LayerRenderer(LayerView view) {
mView = view;
mTasks = new CopyOnWriteArrayList<RenderTask>();
mLastFrameTime = System.nanoTime();
mFrameTimings = new int[60];
mCurrentFrame = mFrameTimingsSum = mDroppedFrames = 0;
mZoomedViewListeners = new ArrayList<LayerView.ZoomedViewListener>();
}
public void destroy() {
if (mCoordByteBuffer != null) {
DirectBufferAllocator.free(mCoordByteBuffer);
mCoordByteBuffer = null;
mCoordBuffer = null;
}
mZoomedViewListeners.clear();
}
void onSurfaceCreated(EGLConfig config) {
createDefaultProgram();
}
public void createDefaultProgram() {
int maxTextureSizeResult[] = new int[1];
GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
mMaxTextureSize = maxTextureSizeResult[0];
}
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);
}
}
}
/** 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 void updateDroppedFrames(long frameStartTime) {
int frameElapsedTime = (int)((System.nanoTime() - frameStartTime) / NANOS_PER_MS);
/* Update the running statistics. */
mFrameTimingsSum -= mFrameTimings[mCurrentFrame];
mFrameTimingsSum += frameElapsedTime;
mDroppedFrames -= (mFrameTimings[mCurrentFrame] + 1) / MAX_FRAME_TIME;
mDroppedFrames += (frameElapsedTime + 1) / MAX_FRAME_TIME;
mFrameTimings[mCurrentFrame] = frameElapsedTime;
mCurrentFrame = (mCurrentFrame + 1) % mFrameTimings.length;
int averageTime = mFrameTimingsSum / mFrameTimings.length;
}
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;
public Frame(ImmutableViewportMetrics metrics) {
mFrameMetrics = metrics;
}
/** This function is invoked via JNI; be careful when modifying signature. */
@WrapForJNI
public void beginDrawing() {
mFrameStartTime = System.nanoTime();
// Run through pre-render tasks
runRenderTasks(mTasks, false, mFrameStartTime);
}
private void maybeRequestZoomedViewRender() {
// 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 = Math.round(mFrameMetrics.getViewport().left);
final float viewTop = Math.round(mFrameMetrics.getViewport().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
public void endDrawing() {
PanningPerfAPI.recordFrameTime();
runRenderTasks(mTasks, true, mFrameStartTime);
maybeRequestZoomedViewRender();
/* Used by robocop for testing purposes */
IntBuffer pixelBuffer = mPixelBuffer;
if (pixelBuffer != null) {
synchronized (pixelBuffer) {
pixelBuffer.position(0);
GLES20.glReadPixels(0, 0, Math.round(mFrameMetrics.getWidth()),
Math.round(mFrameMetrics.getHeight()), 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);
}
}

View file

@ -0,0 +1,711 @@
/* -*- 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.GeckoThread;
import org.mozilla.gecko.mozglue.JNIObject;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.os.Parcelable;
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.FrameLayout;
/**
* A view rendered by the layer compositor.
*/
public class LayerView extends FrameLayout {
private static final String LOGTAG = "GeckoLayerView";
private GeckoLayerClient mLayerClient;
private PanZoomController mPanZoomController;
private DynamicToolbarAnimator mToolbarAnimator;
private LayerRenderer mRenderer;
/* Must be a PAINT_xxx constant */
private int mPaintState;
private FullScreenState mFullScreenState;
private SurfaceView mSurfaceView;
private TextureView mTextureView;
private Listener mListener;
/* This should only be modified on the Java UI thread. */
private final Overscroll mOverscroll;
private boolean mServerSurfaceValid;
private int mWidth, mHeight;
private boolean onAttachedToWindowCalled;
/* This is written by the Gecko thread and the UI thread, and read by the UI thread. */
@WrapForJNI(stubName = "CompositorCreated", calledFrom = "ui")
/* package */ volatile boolean mCompositorCreated;
private class Compositor extends JNIObject {
public Compositor() {
}
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
@Override protected native void disposeNative();
// Gecko thread sets its Java instances; does not block UI thread.
@WrapForJNI(calledFrom = "any", dispatchTo = "gecko")
/* package */ native void attachToJava(GeckoLayerClient layerClient,
NativePanZoomController npzc);
@WrapForJNI(calledFrom = "any", dispatchTo = "gecko")
/* package */ native void onSizeChanged(int windowWidth, int windowHeight,
int screenWidth, int screenHeight);
// Gecko thread creates compositor; blocks UI thread.
@WrapForJNI(calledFrom = "ui", dispatchTo = "proxy")
/* package */ native void createCompositor(int width, int height, Object surface);
// Gecko thread pauses compositor; blocks UI thread.
@WrapForJNI(calledFrom = "ui", dispatchTo = "current")
/* package */ native void syncPauseCompositor();
// UI thread resumes compositor and notifies Gecko thread; does not block UI thread.
@WrapForJNI(calledFrom = "ui", dispatchTo = "current")
/* package */ native void syncResumeResizeCompositor(int width, int height, Object surface);
@WrapForJNI(calledFrom = "any", dispatchTo = "current")
/* package */ native void syncInvalidateAndScheduleComposite();
@WrapForJNI(calledFrom = "gecko")
private void reattach() {
mCompositorCreated = true;
}
@WrapForJNI(calledFrom = "gecko")
private void destroy() {
// The nsWindow has been closed. First mark our compositor as destroyed.
LayerView.this.mCompositorCreated = false;
LayerView.this.mLayerClient.setGeckoReady(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() {
disposeNative();
}
});
}
}
private final Compositor mCompositor = new Compositor();
/* 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;
mFullScreenState = FullScreenState.NONE;
mOverscroll = new OverscrollEdgeEffect(this);
}
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);
}
/**
* 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);
}
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();
}
}
@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();
}
if (mToolbarAnimator != null && mToolbarAnimator.onInterceptTouchEvent(event)) {
if (mPanZoomController != null) {
mPanZoomController.onMotionEventVelocity(event.getEventTime(), mToolbarAnimator.getVelocity());
}
return true;
}
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;
}
if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) {
return true;
}
return false;
}
@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;
}
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 false;
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (AndroidGamepadManager.handleMotionEvent(event)) {
return true;
}
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;
}
if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
return true;
}
return false;
}
@Override
protected void onRestoreInstanceState(final Parcelable state) {
if (onAttachedToWindowCalled) {
attachCompositor();
}
super.onRestoreInstanceState(state);
}
@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);
addView(mSurfaceView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
SurfaceHolder holder = mSurfaceView.getHolder();
holder.addCallback(new SurfaceListener());
}
attachCompositor();
onAttachedToWindowCalled = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
onAttachedToWindowCalled = false;
}
// 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 Matrix getMatrixForLayerRectToViewRect() {
return mLayerClient.getMatrixForLayerRectToViewRect();
}
public void setSurfaceBackgroundColor(int newColor) {
if (mSurfaceView != null) {
mSurfaceView.setBackgroundColor(newColor);
}
}
public void requestRender() {
if (mCompositorCreated) {
mCompositor.syncInvalidateAndScheduleComposite();
}
}
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;
}
private void attachCompositor() {
final NativePanZoomController npzc = (NativePanZoomController) mPanZoomController;
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
mCompositor.attachToJava(mLayerClient, npzc);
} else {
GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
mCompositor, "attachToJava",
GeckoLayerClient.class, mLayerClient,
NativePanZoomController.class, npzc);
}
}
@WrapForJNI(calledFrom = "ui")
protected Object getCompositor() {
return mCompositor;
}
void serverSurfaceChanged(int newWidth, int newHeight) {
ThreadUtils.assertOnUiThread();
mWidth = newWidth;
mHeight = newHeight;
mServerSurfaceValid = true;
updateCompositor();
}
void updateCompositor() {
ThreadUtils.assertOnUiThread();
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).
if (!mServerSurfaceValid) {
return;
}
// 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.
mCompositor.syncResumeResizeCompositor(mWidth, mHeight, getSurface());
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.
if (mServerSurfaceValid && getLayerClient().isGeckoReady()) {
mCompositorCreated = true;
mCompositor.createCompositor(mWidth, mHeight, getSurface());
}
}
/* 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 (!mServerSurfaceValid || mSurfaceView == null) {
surfaceChanged(width, height);
return;
}
if (mCompositorCreated) {
mCompositor.syncResumeResizeCompositor(width, height, getSurface());
}
if (mOverscroll != null) {
mOverscroll.setSize(width, height);
}
}
private void surfaceChanged(int width, int height) {
serverSurfaceChanged(width, height);
if (mListener != null) {
mListener.surfaceChanged(width, height);
}
if (mOverscroll != null) {
mOverscroll.setSize(width, height);
}
}
void notifySizeChanged(int windowWidth, int windowHeight, int screenWidth, int screenHeight) {
mCompositor.onSizeChanged(windowWidth, windowHeight, screenWidth, screenHeight);
}
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) {
mCompositor.syncPauseCompositor();
}
mServerSurfaceValid = false;
}
private void onDestroyed() {
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(calledFrom = "gecko")
private 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 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();
}
}
/* A subclass of SurfaceView to listen to layout changes, as
* View.OnLayoutChangeListener requires API level 11.
*/
private class LayerSurfaceView extends SurfaceView {
private LayerView mParent;
public LayerSurfaceView(Context aContext, LayerView aParent) {
super(aContext);
mParent = aParent;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed && mParent.mServerSurfaceValid) {
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);
}
@Override
public int 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(gainFocus);
}
public void setFullScreenState(FullScreenState state) {
mFullScreenState = state;
}
public boolean isFullScreen() {
return mFullScreenState != FullScreenState.NONE;
}
public void setMaxTranslation(float aMaxTranslation) {
mToolbarAnimator.setMaxTranslation(aMaxTranslation);
}
public void setSurfaceTranslation(float translation) {
setTranslationY(translation);
}
public float getSurfaceTranslation() {
return getTranslationY();
}
// 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);
}
public void setClearColor(int color) {
if (mLayerClient != null) {
mLayerClient.setClearColor(color);
}
}
}

View file

@ -0,0 +1,300 @@
/* -*- 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.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 PrefsHelper.PrefHandler mPrefsObserver;
private long mLastDownTime;
private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
@WrapForJNI(calledFrom = "ui")
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(calledFrom = "ui")
private native boolean handleScrollEvent(
long time, int metaState,
float x, float y,
float hScroll, float vScroll);
@WrapForJNI(calledFrom = "ui")
private native boolean handleMouseEvent(
int action, long time, int metaState,
float x, float y, int buttons);
@WrapForJNI(calledFrom = "ui")
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;
mDestroyed = true;
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) {
// NOTE: This commented out block of code allows Fennec to generate
// mouse event instead of converting them to touch events.
// This gives Fennec similar behaviour to desktop when using
// a mouse.
//
// if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
// return handleMouseEvent(event);
// } else {
// return handleMotionEvent(event);
// }
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 void onMotionEventVelocity(final long aEventTime, final float aSpeedY) {
handleMotionEventVelocity(aEventTime, aSpeedY);
}
@Override @WrapForJNI(calledFrom = "ui") // PanZoomController
public void destroy() {
if (mPrefsObserver != null) {
PrefsHelper.removeObserver(mPrefsObserver);
mPrefsObserver = null;
}
if (mDestroyed || !mTarget.isGeckoReady()) {
return;
}
mDestroyed = true;
disposeNative();
}
@Override
public void attach() {
mDestroyed = false;
}
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") @Override // JNIObject
protected native void disposeNative();
@Override
public void setOverscrollHandler(final Overscroll handler) {
mOverscroll = handler;
}
@WrapForJNI(stubName = "SetIsLongpressEnabled") // Called from test thread.
private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
@Override // PanZoomController
public void setIsLongpressEnabled(boolean isLongpressEnabled) {
if (!mDestroyed) {
nativeSetIsLongpressEnabled(isLongpressEnabled);
}
}
@WrapForJNI(calledFrom = "ui")
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
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
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(calledFrom = "ui")
private void setScrollingRootContent(final boolean isRootContent) {
mTarget.setScrollingRootContent(isRootContent);
}
/**
* Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
* to avoid unwanted scroll interactions.
*/
@WrapForJNI(calledFrom = "gecko")
private void onSelectionDragState(boolean state) {
mView.getDynamicToolbarAnimator().setPinned(state, PinReason.CARET_DRAG);
}
}

View file

@ -0,0 +1,21 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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);
}

View file

@ -0,0 +1,162 @@
/* -*- 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;
}
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, 0, 0);
}
if (!mEdges[BOTTOM].isFinished()) {
invalidate |= draw(mEdges[BOTTOM], canvas, visibleEnd.x, visibleEnd.y, 180);
}
if (!mEdges[LEFT].isFinished()) {
invalidate |= draw(mEdges[LEFT], canvas, 0, visibleEnd.y, 270);
}
if (!mEdges[RIGHT].isFinished()) {
invalidate |= draw(mEdges[RIGHT], canvas, visibleEnd.x, 0, 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;
}
}

View file

@ -0,0 +1,38 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.gfx;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.EventDispatcher;
import android.graphics.PointF;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
public interface PanZoomController {
// 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) {
return new NativePanZoomController(target, view);
}
}
public void destroy();
public void attach();
public boolean onTouchEvent(MotionEvent event);
public boolean onMotionEvent(MotionEvent event);
public void onMotionEventVelocity(final long aEventTime, final float aSpeedY);
public void setOverscrollHandler(final Overscroll controller);
public void setIsLongpressEnabled(boolean isLongpressEnabled);
public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift);
}

View file

@ -0,0 +1,15 @@
/* -*- 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.Matrix;
import android.graphics.PointF;
public interface PanZoomTarget {
public void panZoomStopped();
public boolean isGeckoReady();
public void setScrollingRootContent(boolean isRootContent);
}

View file

@ -0,0 +1,73 @@
/* -*- 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 void initialiseRecordingArrays() {
if (mFrameTimes == null) {
mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
} else {
mFrameTimes.clear();
}
}
@RobocopTarget
public static void startFrameTimeRecording() {
if (mRecordingFrames) {
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);
}
}
@RobocopTarget
public static void startCheckerboardRecording() {
throw new UnsupportedOperationException();
}
@RobocopTarget
public static List<Float> stopCheckerboardRecording() {
throw new UnsupportedOperationException();
}
}

View file

@ -0,0 +1,51 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,695 @@
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.mozilla.gecko.gfx;
import android.content.Context;
import android.hardware.SensorManager;
import android.util.Log;
import android.view.ViewConfiguration;
import org.mozilla.gecko.annotation.WrapForJNI;
/**
* This class is vastly copied from {@link android.widget.OverScroller} but decouples the time
* from the app time so it can be specified manually.
*/
@WrapForJNI(exceptionMode = "nsresult")
public class StackScroller {
private int mMode;
private final SplineStackScroller mScrollerX;
private final SplineStackScroller mScrollerY;
private final boolean mFlywheel;
private static final int SCROLL_MODE = 0;
private static final int FLING_MODE = 1;
private static float sViscousFluidScale;
private static float sViscousFluidNormalize;
/**
* Creates an StackScroller with a viscous fluid scroll interpolator and flywheel.
* @param context
*/
public StackScroller(Context context) {
mFlywheel = true;
mScrollerX = new SplineStackScroller(context);
mScrollerY = new SplineStackScroller(context);
initContants();
}
private static void initContants() {
// This controls the viscous fluid effect (how much of it)
sViscousFluidScale = 8.0f;
// must be set to 1.0 (used in viscousFluid())
sViscousFluidNormalize = 1.0f;
sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
}
/**
*
* Returns whether the scroller has finished scrolling.
*
* @return True if the scroller has finished scrolling, false otherwise.
*/
public final boolean isFinished() {
return mScrollerX.mFinished && mScrollerY.mFinished;
}
/**
* Force the finished field to a particular value. Contrary to
* {@link #abortAnimation()}, forcing the animation to finished
* does NOT cause the scroller to move to the final x and y
* position.
*
* @param finished The new finished value.
*/
public final void forceFinished(boolean finished) {
mScrollerX.mFinished = mScrollerY.mFinished = finished;
}
/**
* Returns the current X offset in the scroll.
*
* @return The new X offset as an absolute distance from the origin.
*/
public final int getCurrX() {
return mScrollerX.mCurrentPosition;
}
/**
* Returns the current Y offset in the scroll.
*
* @return The new Y offset as an absolute distance from the origin.
*/
public final int getCurrY() {
return mScrollerY.mCurrentPosition;
}
/**
* Returns where the scroll will end. Valid only for "fling" scrolls.
*
* @return The final X offset as an absolute distance from the origin.
*/
public final int getFinalX() {
return mScrollerX.mFinal;
}
public final float getCurrSpeedX() {
return mScrollerX.mCurrVelocity;
}
public final float getCurrSpeedY() {
return mScrollerY.mCurrVelocity;
}
/**
* Returns where the scroll will end. Valid only for "fling" scrolls.
*
* @return The final Y offset as an absolute distance from the origin.
*/
public final int getFinalY() {
return mScrollerY.mFinal;
}
/**
* Sets where the scroll will end. Valid only for "fling" scrolls.
*
* @param x The final X offset as an absolute distance from the origin.
*/
public final void setFinalX(int x) {
mScrollerX.setFinalPosition(x);
}
private static float viscousFluid(float x) {
x *= sViscousFluidScale;
if (x < 1.0f) {
x -= (1.0f - (float) Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float) Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
x *= sViscousFluidNormalize;
return x;
}
/**
* Call this when you want to know the new location. If it returns true, the
* animation is not yet finished.
*/
public boolean computeScrollOffset(long time) {
if (isFinished()) {
return false;
}
switch (mMode) {
case SCROLL_MODE:
// Any scroller can be used for time, since they were started
// together in scroll mode. We use X here.
final long elapsedTime = time - mScrollerX.mStartTime;
final int duration = mScrollerX.mDuration;
if (elapsedTime < duration) {
float q = (float) (elapsedTime) / duration;
q = viscousFluid(q);
mScrollerX.updateScroll(q);
mScrollerY.updateScroll(q);
} else {
abortAnimation();
}
break;
case FLING_MODE:
if (!mScrollerX.mFinished) {
if (!mScrollerX.update(time)) {
if (!mScrollerX.continueWhenFinished(time)) {
mScrollerX.finish();
}
}
}
if (!mScrollerY.mFinished) {
if (!mScrollerY.update(time)) {
if (!mScrollerY.continueWhenFinished(time)) {
mScrollerY.finish();
}
}
}
break;
default:
break;
}
return true;
}
/**
* Start scrolling by providing a starting point and the distance to travel.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, long startTime, int duration) {
mMode = SCROLL_MODE;
mScrollerX.startScroll(startX, dx, startTime, duration);
mScrollerY.startScroll(startY, dy, startTime, duration);
}
/**
* Call this when you want to 'spring back' into a valid coordinate range.
*
* @param startX Starting X coordinate
* @param startY Starting Y coordinate
* @param minX Minimum valid X value
* @param maxX Maximum valid X value
* @param minY Minimum valid Y value
* @param maxY Minimum valid Y value
* @return true if a springback was initiated, false if startX and startY were
* already within the valid range.
*/
public boolean springBack(
int startX, int startY, int minX, int maxX, int minY, int maxY, long time) {
mMode = FLING_MODE;
// Make sure both methods are called.
final boolean spingbackX = mScrollerX.springback(startX, minX, maxX, time);
final boolean spingbackY = mScrollerY.springback(startY, minY, maxY, time);
return spingbackX || spingbackY;
}
/**
* Start scrolling based on a fling gesture. The distance traveled will
* depend on the initial velocity of the fling.
*
* @param startX Starting point of the scroll (X)
* @param startY Starting point of the scroll (Y)
* @param velocityX Initial velocity of the fling (X) measured in pixels per second.
* @param velocityY Initial velocity of the fling (Y) measured in pixels per second
* @param minX Minimum X value. The scroller will not scroll past this point
* unless overX > 0. If overfling is allowed, it will use minX as
* a springback boundary.
* @param maxX Maximum X value. The scroller will not scroll past this point
* unless overX > 0. If overfling is allowed, it will use maxX as
* a springback boundary.
* @param minY Minimum Y value. The scroller will not scroll past this point
* unless overY > 0. If overfling is allowed, it will use minY as
* a springback boundary.
* @param maxY Maximum Y value. The scroller will not scroll past this point
* unless overY > 0. If overfling is allowed, it will use maxY as
* a springback boundary.
* @param overX Overfling range. If > 0, horizontal overfling in either
* direction will be possible.
* @param overY Overfling range. If > 0, vertical overfling in either
* direction will be possible.
*/
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX,
int minY, int maxY, int overX, int overY, long time) {
// Continue a scroll or fling in progress
if (mFlywheel && !isFinished()) {
float oldVelocityX = mScrollerX.mCurrVelocity;
float oldVelocityY = mScrollerY.mCurrVelocity;
boolean sameXDirection = (velocityX == 0) || (oldVelocityX == 0) ||
((velocityX < 0) == (oldVelocityX < 0));
boolean sameYDirection = (velocityY == 0) || (oldVelocityY == 0) ||
((velocityY < 0) == (oldVelocityY < 0));
if (sameXDirection) {
velocityX += oldVelocityX;
}
if (sameYDirection) {
velocityY += oldVelocityY;
}
}
mMode = FLING_MODE;
mScrollerX.fling(startX, velocityX, minX, maxX, overX, time);
mScrollerY.fling(startY, velocityY, minY, maxY, overY, time);
}
/**
* Stops the animation. Contrary to {@link #forceFinished(boolean)},
* aborting the animating causes the scroller to move to the final x and y
* positions.
*
* @see #forceFinished(boolean)
*/
public void abortAnimation() {
mScrollerX.finish();
mScrollerY.finish();
}
static class SplineStackScroller {
// Initial position
private int mStart;
// Current position
private int mCurrentPosition;
// Final position
private int mFinal;
// Initial velocity
private int mVelocity;
// Current velocity
private float mCurrVelocity;
// Constant current deceleration
private float mDeceleration;
// Animation starting time, in system milliseconds
private long mStartTime;
// Animation duration, in milliseconds
private int mDuration;
// Duration to complete spline component of animation
private int mSplineDuration;
// Distance to travel along spline animation
private int mSplineDistance;
// Whether the animation is currently in progress
private boolean mFinished;
// The allowed overshot distance before boundary is reached.
private int mOver;
// Fling friction
private final float mFlingFriction = ViewConfiguration.getScrollFriction();
// Current state of the animation.
private int mState = SPLINE;
// Constant gravity value, used in the deceleration phase.
private static final float GRAVITY = 2000.0f;
// A context-specific coefficient adjusted to physical values.
private final float mPhysicalCoeff;
private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
private static final float START_TENSION = 0.5f;
private static final float END_TENSION = 1.0f;
private static final float P1 = START_TENSION * INFLEXION;
private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
private static final int NB_SAMPLES = 100;
private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
private static final int SPLINE = 0;
private static final int CUBIC = 1;
private static final int BALLISTIC = 2;
static {
float xMin = 0.0f;
float yMin = 0.0f;
for (int i = 0; i < NB_SAMPLES; i++) {
final float alpha = (float) i / NB_SAMPLES;
float xMax = 1.0f;
float x, tx, coef;
while (true) {
x = xMin + (xMax - xMin) / 2.0f;
coef = 3.0f * x * (1.0f - x);
tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
if (Math.abs(tx - alpha) < 1E-5) break;
if (tx > alpha) {
xMax = x;
} else {
xMin = x;
}
}
SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
float yMax = 1.0f;
float y, dy;
while (true) {
y = yMin + (yMax - yMin) / 2.0f;
coef = 3.0f * y * (1.0f - y);
dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
if (Math.abs(dy - alpha) < 1E-5) break;
if (dy > alpha) {
yMax = y;
} else {
yMin = y;
}
}
SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
}
SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
}
SplineStackScroller(Context context) {
mFinished = true;
final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* ppi * 0.84f; // look and feel tuning
}
void updateScroll(float q) {
mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
}
/*
* Get a signed deceleration that will reduce the velocity.
*/
private static float getDeceleration(int velocity) {
return velocity > 0 ? -GRAVITY : GRAVITY;
}
/*
* Modifies mDuration to the duration it takes to get from start to newFinal using the
* spline interpolation. The previous duration was needed to get to oldFinal.
*/
private void adjustDuration(int start, int oldFinal, int newFinal) {
final int oldDistance = oldFinal - start;
final int newDistance = newFinal - start;
final float x = Math.abs((float) newDistance / oldDistance);
final int index = (int) (NB_SAMPLES * x);
if (index < NB_SAMPLES) {
final float xInf = (float) index / NB_SAMPLES;
final float xSup = (float) (index + 1) / NB_SAMPLES;
final float tInf = SPLINE_TIME[index];
final float tSup = SPLINE_TIME[index + 1];
final float timeCoef = tInf + (x - xInf) / (xSup - xInf) * (tSup - tInf);
mDuration *= timeCoef;
}
}
void startScroll(int start, int distance, long startTime, int duration) {
mFinished = false;
mStart = start;
mFinal = start + distance;
mStartTime = startTime;
mDuration = duration;
// Unused
mDeceleration = 0.0f;
mVelocity = 0;
}
void finish() {
mCurrentPosition = mFinal;
// Not reset since WebView relies on this value for fast fling.
// TODO: restore when WebView uses the fast fling implemented in this class.
// mCurrVelocity = 0.0f;
mFinished = true;
}
void setFinalPosition(int position) {
mFinal = position;
mFinished = false;
}
boolean springback(int start, int min, int max, long time) {
mFinished = true;
mStart = mFinal = start;
mVelocity = 0;
mStartTime = time;
mDuration = 0;
if (start < min) {
startSpringback(start, min, 0);
} else if (start > max) {
startSpringback(start, max, 0);
}
return !mFinished;
}
private void startSpringback(int start, int end, int velocity) {
// mStartTime has been set
mFinished = false;
mState = CUBIC;
mStart = start;
mFinal = end;
final int delta = start - end;
mDeceleration = getDeceleration(delta);
// TODO take velocity into account
mVelocity = -delta; // only sign is used
mOver = Math.abs(delta);
mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
}
void fling(int start, int velocity, int min, int max, int over, long time) {
mOver = over;
mFinished = false;
mCurrVelocity = mVelocity = velocity;
mDuration = mSplineDuration = 0;
mStartTime = time;
mCurrentPosition = mStart = start;
if (start > max || start < min) {
startAfterEdge(start, min, max, velocity, time);
return;
}
mState = SPLINE;
double totalDistance = 0.0;
if (velocity != 0) {
mDuration = mSplineDuration = getSplineFlingDuration(velocity);
totalDistance = getSplineFlingDistance(velocity);
}
mSplineDistance = (int) (totalDistance * Math.signum(velocity));
mFinal = start + mSplineDistance;
// Clamp to a valid final position
if (mFinal < min) {
adjustDuration(mStart, mFinal, min);
mFinal = min;
}
if (mFinal > max) {
adjustDuration(mStart, mFinal, max);
mFinal = max;
}
}
private double getSplineDeceleration(int velocity) {
return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
}
private double getSplineFlingDistance(int velocity) {
final double l = getSplineDeceleration(velocity);
final double decelMinusOne = DECELERATION_RATE - 1.0;
return mFlingFriction * mPhysicalCoeff
* Math.exp(DECELERATION_RATE / decelMinusOne * l);
}
/* Returns the duration, expressed in milliseconds */
private int getSplineFlingDuration(int velocity) {
final double l = getSplineDeceleration(velocity);
final double decelMinusOne = DECELERATION_RATE - 1.0;
return (int) (1000.0 * Math.exp(l / decelMinusOne));
}
private void fitOnBounceCurve(int start, int end, int velocity) {
// Simulate a bounce that started from edge
final float durationToApex = -velocity / mDeceleration;
final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration);
final float distanceToEdge = Math.abs(end - start);
final float totalDuration = (float) Math.sqrt(
2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
mStart = end;
mVelocity = (int) (-mDeceleration * totalDuration);
}
private void startBounceAfterEdge(int start, int end, int velocity) {
mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
fitOnBounceCurve(start, end, velocity);
onEdgeReached();
}
private void startAfterEdge(int start, int min, int max, int velocity, long time) {
if (start > min && start < max) {
Log.e("StackScroller", "startAfterEdge called from a valid position");
mFinished = true;
return;
}
final boolean positive = start > max;
final int edge = positive ? max : min;
final int overDistance = start - edge;
boolean keepIncreasing = overDistance * velocity >= 0;
if (keepIncreasing) {
// Will result in a bounce or a to_boundary depending on velocity.
startBounceAfterEdge(start, edge, velocity);
} else {
final double totalDistance = getSplineFlingDistance(velocity);
if (totalDistance > Math.abs(overDistance)) {
fling(start, velocity, positive ? min : start, positive ? start : max, mOver,
time);
} else {
startSpringback(start, edge, velocity);
}
}
}
private void onEdgeReached() {
// mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration));
final float sign = Math.signum(mVelocity);
if (distance > mOver) {
// Default deceleration is not sufficient to slow us down before boundary
mDeceleration = -sign * mVelocity * mVelocity / (2.0f * mOver);
distance = mOver;
}
mOver = (int) distance;
mState = BALLISTIC;
mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
mDuration = -(int) (1000.0f * mVelocity / mDeceleration);
}
boolean continueWhenFinished(long time) {
switch (mState) {
case SPLINE:
// Duration from start to null velocity
if (mDuration < mSplineDuration) {
// If the animation was clamped, we reached the edge
mStart = mFinal;
// TODO Better compute speed when edge was reached
mVelocity = (int) mCurrVelocity;
mDeceleration = getDeceleration(mVelocity);
mStartTime += mDuration;
onEdgeReached();
} else {
// Normal stop, no need to continue
return false;
}
break;
case BALLISTIC:
mStartTime += mDuration;
startSpringback(mFinal, mStart, 0);
break;
case CUBIC:
return false;
}
update(time);
return true;
}
/*
* Update the current position and velocity for current time. Returns
* true if update has been done and false if animation duration has been
* reached.
*/
boolean update(long time) {
final long currentTime = time - mStartTime;
if (((mState == SPLINE) && (mSplineDuration <= 0)) ||
((mState == CUBIC) && (mDuration <= 0))) {
return false;
}
if (currentTime > mDuration) {
return false;
}
double distance = 0.0;
switch (mState) {
case SPLINE: {
final float t = (float) currentTime / mSplineDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float tInf = (float) index / NB_SAMPLES;
final float tSup = (float) (index + 1) / NB_SAMPLES;
final float dInf = SPLINE_POSITION[index];
final float dSup = SPLINE_POSITION[index + 1];
velocityCoef = (dSup - dInf) / (tSup - tInf);
distanceCoef = dInf + (t - tInf) * velocityCoef;
}
distance = distanceCoef * mSplineDistance;
mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
break;
}
case BALLISTIC: {
final float t = currentTime / 1000.0f;
mCurrVelocity = mVelocity + mDeceleration * t;
distance = mVelocity * t + mDeceleration * t * t / 2.0f;
break;
}
case CUBIC: {
final float t = (float) (currentTime) / mDuration;
final float t2 = t * t;
final float sign = Math.signum(mVelocity);
distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
mCurrVelocity = sign * mOver * 6.0f * (-t + t2);
break;
}
}
mCurrentPosition = mStart + (int) Math.round(distance);
return true;
}
}
}

View file

@ -0,0 +1,38 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.mozglue.JNIObject;
import android.graphics.SurfaceTexture;
final class SurfaceTextureListener
extends JNIObject implements SurfaceTexture.OnFrameAvailableListener
{
@WrapForJNI(calledFrom = "gecko")
private SurfaceTextureListener() {
}
@Override
protected void disposeNative() {
// SurfaceTextureListener is disposed inside AndroidSurfaceTexture.
throw new IllegalStateException("unreachable code");
}
@WrapForJNI(stubName = "OnFrameAvailable")
private native void nativeOnFrameAvailable();
@Override // SurfaceTexture.OnFrameAvailableListener
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
try {
nativeOnFrameAvailable();
} catch (final NullPointerException e) {
// Ignore exceptions caused by a disposed object, i.e.
// getting a callback after this listener is no longer in use.
}
}
}

View file

@ -0,0 +1,28 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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;
}
}

View file

@ -0,0 +1,64 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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() {
// Do nothing, we need to keep the native references around for child
// buffers.
}
@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;
}
}

View file

@ -0,0 +1,52 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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);
}

View file

@ -0,0 +1,549 @@
/* -*- 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");
}
}
putenv("MOZ_LINKER_EXTRACT=1");
}
@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);
}
public synchronized static void extractGeckoLibs(final Context context, final String apkName) {
loadLibsSetupLocked(context);
try {
extractGeckoLibsNative(apkName);
} catch (Exception e) {
Log.e(LOGTAG, "Failing library extraction.", e);
}
}
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);
private static native void extractGeckoLibsNative(String apkName);
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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.mozglue;
import android.support.annotation.Keep;
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;
@Keep
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
protected void finalize() {
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;
}
}

View file

@ -0,0 +1,134 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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;
import java.util.ArrayList;
/**
* 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 hasExtra(String name) {
try {
return intent.hasExtra(name);
} catch (OutOfMemoryError e) {
Log.w(LOGTAG, "Couldn't determine if intent had an extra: OOM. Malformed?");
return false;
} catch (RuntimeException e) {
Log.w(LOGTAG, "Couldn't determine if intent had an extra.", e);
return false;
}
}
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 int getIntExtra(final String name, final int defaultValue) {
try {
return intent.getIntExtra(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 ArrayList<String> getStringArrayListExtra(final String name) {
try {
return intent.getStringArrayListExtra(name);
} 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;
}
}

View file

@ -0,0 +1,133 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,38 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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;
}
}

View file

@ -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.JNITarget;
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
*/
@JNITarget
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
*/
@JNITarget
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
*/
@JNITarget
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
*/
@JNITarget
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}. */
@JNITarget
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();
}
}

View file

@ -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();
}
}
}

View file

@ -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.JNITarget;
@JNITarget
public class SQLiteBridgeException extends RuntimeException {
static final long serialVersionUID = 1L;
public SQLiteBridgeException() {}
public SQLiteBridgeException(String msg) {
super(msg);
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -0,0 +1,72 @@
/* -*- 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.content.Intent;
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) {
int newVis;
if (fullscreen) {
newVis = View.SYSTEM_UI_FLAG_FULLSCREEN;
if (Versions.feature19Plus) {
newVis |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
} else {
newVis |= 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);
}
/**
* Finish this activity and launch the default home screen activity.
*/
public static void goToHomeScreen(Activity activity) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
}
}

View file

@ -0,0 +1,25 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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);
}

View file

@ -0,0 +1,117 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy 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(calledFrom = "gecko")
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(calledFrom = "gecko")
public static void setText(final CharSequence text) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
// which is a subclass of android.text.ClipboardManager.
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;
}
});
}
/**
* @return true if the clipboard is nonempty, false otherwise.
*/
@WrapForJNI(calledFrom = "gecko")
public static boolean hasText() {
android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
return cm.hasPrimaryClip();
}
/**
* Deletes all text from the clipboard.
*/
@WrapForJNI(calledFrom = "gecko")
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() {
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();
}
}
return null;
}
}

View file

@ -0,0 +1,51 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy 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;
import android.text.TextUtils;
public class ContextUtils {
private static final String INSTALLER_GOOGLE_PLAY = "com.android.vending";
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");
}
}
public static boolean isPackageInstalled(final Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
pm.getPackageInfo(packageName, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
public static boolean isInstalledFromGooglePlay(final Context context) {
final String installerPackageName = context.getPackageManager().getInstallerPackageName(context.getPackageName());
if (TextUtils.isEmpty(installerPackageName)) {
return false;
}
return INSTALLER_GOOGLE_PLAY.equals(installerPackageName);
}
}

View file

@ -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));
}
}

View file

@ -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);
}

View file

@ -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;
}
}
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -0,0 +1,261 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.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);
}
}
}
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);
}
}
}
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;
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,169 @@
/* -*- 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";
private static final String VP9_MIME_TYPE = "video/x-vnd.on2.vp9";
// 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
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() {
return getHWDecoderCapability(VP8_MIME_TYPE);
}
@WrapForJNI
public static boolean HasHWVP9() {
return getHWDecoderCapability(VP9_MIME_TYPE);
}
public static boolean getHWDecoderCapability(String aMimeType) {
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(aMimeType)) {
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(aMimeType);
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.
}
}

View file

@ -0,0 +1,117 @@
/* -*- 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();
}
public static boolean isARMSystem() {
return Build.CPU_ABI != null && Build.CPU_ABI.equals("armeabi-v7a");
}
public static boolean isX86System() {
return Build.CPU_ABI != null && Build.CPU_ABI.equals("x86");
}
/**
* @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
final boolean isSystemARM = isARMSystem();
final boolean isSystemX86 = isX86System();
final boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("armeabi-v7a");
final 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;
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,109 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mozilla.gecko.util;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.text.TextUtils;
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 SafeIntent intent) {
// 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 HashMap<String, String> out = new HashMap<>();
int i = 0;
while (true) {
final String envKey = "env" + i;
i += 1;
if (!intent.hasExtra(envKey)) {
break;
}
maybeAddEnvVarToEnvVarMap(out, intent, 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);
}
/**
* Gets whether or not we're in automation from the passed in environment variables.
*
* We need to read environment variables from the intent string
* extra because environment variables from our test harness aren't set
* until Gecko is loaded, and we need to know this before then.
*
* The return value of this method should be used early since other
* initialization may depend on its results.
*/
@CheckResult
public static boolean getIsInAutomationFromEnvironment(final SafeIntent intent) {
final HashMap<String, String> envVars = IntentUtils.getEnvVarMap(intent);
return !TextUtils.isEmpty(envVars.get(IntentUtils.ENV_VAR_IN_AUTOMATION));
}
}

View file

@ -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;
}
}

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