mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-21 07:47:32 +09:00
pt 1 in reviving the android build (copied from pm 28a1, wish me luck)
This commit is contained in:
parent
efa9662725
commit
d7788a6d6d
4249 changed files with 468189 additions and 0 deletions
176
mobile/android/geckoview/build.gradle
Normal file
176
mobile/android/geckoview/build.gradle
Normal 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
|
||||
}
|
||||
}
|
||||
175
mobile/android/geckoview/proguard-rules.txt
Normal file
175
mobile/android/geckoview/proguard-rules.txt
Normal 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>;
|
||||
}
|
||||
39
mobile/android/geckoview/src/main/AndroidManifest.xml
Normal file
39
mobile/android/geckoview/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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 {}
|
||||
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue